Source file src/net/http/fs.go

     1  // Copyright 2009 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // HTTP file system request handler
     6  
     7  package http
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  	"internal/godebug"
    13  	"io"
    14  	"io/fs"
    15  	"mime"
    16  	"mime/multipart"
    17  	"net/http/internal"
    18  	"net/http/internal/ascii"
    19  	"net/textproto"
    20  	"net/url"
    21  	"os"
    22  	"path"
    23  	"path/filepath"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  )
    29  
    30  // A Dir implements [FileSystem] using the local filesystem.
    31  // Most users should prefer using [FileServerFS] with an [os.Root] rather than Dir.
    32  //
    33  // Dir's string value names a local directory path to serve.
    34  // An empty Dir is treated as ".".
    35  //
    36  // Dir will follow symbolic links, including links pointing outside its directory.
    37  //
    38  // Dir will serve files starting with a dot, which can expose sensitive
    39  // directories such as .git or sensitive files such as .htpassword.
    40  //
    41  // See [FileServerFS] for examples of restricting access to a directory using [os.Root],
    42  // and of hiding files starting with a dot.
    43  type Dir string
    44  
    45  // mapOpenError maps the provided non-nil error from opening name
    46  // to a possibly better non-nil error. In particular, it turns OS-specific errors
    47  // about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552.
    48  func mapOpenError(originalErr error, name string, sep rune, stat func(string) (fs.FileInfo, error)) error {
    49  	if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
    50  		return originalErr
    51  	}
    52  
    53  	parts := strings.Split(name, string(sep))
    54  	for i := range parts {
    55  		if parts[i] == "" {
    56  			continue
    57  		}
    58  		fi, err := stat(strings.Join(parts[:i+1], string(sep)))
    59  		if err != nil {
    60  			return originalErr
    61  		}
    62  		if !fi.IsDir() {
    63  			return fs.ErrNotExist
    64  		}
    65  	}
    66  	return originalErr
    67  }
    68  
    69  // errInvalidUnsafePath is returned by Dir.Open when the call to
    70  // filepath.Localize fails. filepath.Localize returns an error if the path
    71  // cannot be represented by the operating system.
    72  var errInvalidUnsafePath = errors.New("http: invalid or unsafe file path")
    73  
    74  // Open implements [FileSystem] using [os.Open], opening files for reading rooted
    75  // and relative to the directory d.
    76  func (d Dir) Open(name string) (File, error) {
    77  	path := path.Clean("/" + name)[1:]
    78  	if path == "" {
    79  		path = "."
    80  	}
    81  	path, err := filepath.Localize(path)
    82  	if err != nil {
    83  		return nil, errInvalidUnsafePath
    84  	}
    85  	dir := string(d)
    86  	if dir == "" {
    87  		dir = "."
    88  	}
    89  	fullName := filepath.Join(dir, path)
    90  	f, err := os.Open(fullName)
    91  	if err != nil {
    92  		return nil, mapOpenError(err, fullName, filepath.Separator, os.Stat)
    93  	}
    94  	return f, nil
    95  }
    96  
    97  // A FileSystem implements access to a collection of named files.
    98  // Most users should prefer using [FileServerFS] rather than FileSystem.
    99  //
   100  // The elements in a file path are separated by slash ('/', U+002F)
   101  // characters, regardless of host operating system convention.
   102  // See the [FileServer] function to convert a FileSystem to a [Handler].
   103  //
   104  // This interface predates the [fs.FS] interface, which can be used instead:
   105  // the [FS] adapter function converts an fs.FS to a FileSystem.
   106  type FileSystem interface {
   107  	Open(name string) (File, error)
   108  }
   109  
   110  // A File is returned by a [FileSystem]'s Open method.
   111  // Most users should prefer using [FileServerFS] rather than FileSystem.
   112  //
   113  // The methods of File behave the same as those on an [*os.File].
   114  type File interface {
   115  	io.Closer
   116  	io.Reader
   117  	io.Seeker
   118  	Readdir(count int) ([]fs.FileInfo, error)
   119  	Stat() (fs.FileInfo, error)
   120  }
   121  
   122  type anyDirs interface {
   123  	len() int
   124  	name(i int) string
   125  	isDir(i int) bool
   126  }
   127  
   128  type fileInfoDirs []fs.FileInfo
   129  
   130  func (d fileInfoDirs) len() int          { return len(d) }
   131  func (d fileInfoDirs) isDir(i int) bool  { return d[i].IsDir() }
   132  func (d fileInfoDirs) name(i int) string { return d[i].Name() }
   133  
   134  type dirEntryDirs []fs.DirEntry
   135  
   136  func (d dirEntryDirs) len() int          { return len(d) }
   137  func (d dirEntryDirs) isDir(i int) bool  { return d[i].IsDir() }
   138  func (d dirEntryDirs) name(i int) string { return d[i].Name() }
   139  
   140  func dirList(w ResponseWriter, r *Request, f File) {
   141  	// Prefer to use ReadDir instead of Readdir,
   142  	// because the former doesn't require calling
   143  	// Stat on every entry of a directory on Unix.
   144  	var dirs anyDirs
   145  	var err error
   146  	if d, ok := f.(fs.ReadDirFile); ok {
   147  		var list dirEntryDirs
   148  		list, err = d.ReadDir(-1)
   149  		dirs = list
   150  	} else {
   151  		var list fileInfoDirs
   152  		list, err = f.Readdir(-1)
   153  		dirs = list
   154  	}
   155  
   156  	if err != nil {
   157  		logf(r, "http: error reading directory: %v", err)
   158  		Error(w, "Error reading directory", StatusInternalServerError)
   159  		return
   160  	}
   161  	sort.Slice(dirs, func(i, j int) bool { return dirs.name(i) < dirs.name(j) })
   162  
   163  	w.Header().Set("Content-Type", "text/html; charset=utf-8")
   164  	fmt.Fprintf(w, "<!doctype html>\n")
   165  	fmt.Fprintf(w, "<meta name=\"viewport\" content=\"width=device-width\">\n")
   166  	fmt.Fprintf(w, "<meta name=\"color-scheme\" content=\"light dark\">\n")
   167  	fmt.Fprintf(w, "<pre>\n")
   168  	for i, n := 0, dirs.len(); i < n; i++ {
   169  		name := dirs.name(i)
   170  		if dirs.isDir(i) {
   171  			name += "/"
   172  		}
   173  		// name may contain '?' or '#', which must be escaped to remain
   174  		// part of the URL path, and not indicate the start of a query
   175  		// string or fragment.
   176  		url := url.URL{Path: name}
   177  		fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name))
   178  	}
   179  	fmt.Fprintf(w, "</pre>\n")
   180  }
   181  
   182  // GODEBUG=httpservecontentkeepheaders=1 restores the pre-1.23 behavior of not deleting
   183  // Cache-Control, Content-Encoding, Etag, or Last-Modified headers on ServeContent errors.
   184  var httpservecontentkeepheaders = godebug.New("httpservecontentkeepheaders")
   185  
   186  // serveError serves an error from ServeFile, ServeFileFS, and ServeContent.
   187  // Because those can all be configured by the caller by setting headers like
   188  // Etag, Last-Modified, and Cache-Control to send on a successful response,
   189  // the error path needs to clear them, since they may not be meant for errors.
   190  func serveError(w ResponseWriter, text string, code int) {
   191  	h := w.Header()
   192  
   193  	nonDefault := false
   194  	for _, k := range []string{
   195  		"Cache-Control",
   196  		"Content-Encoding",
   197  		"Etag",
   198  		"Last-Modified",
   199  	} {
   200  		if !h.has(k) {
   201  			continue
   202  		}
   203  		if httpservecontentkeepheaders.Value() == "1" {
   204  			nonDefault = true
   205  		} else {
   206  			h.Del(k)
   207  		}
   208  	}
   209  	if nonDefault {
   210  		httpservecontentkeepheaders.IncNonDefault()
   211  	}
   212  
   213  	Error(w, text, code)
   214  }
   215  
   216  // ServeContent replies to the request using the content in the
   217  // provided ReadSeeker. The main benefit of ServeContent over [io.Copy]
   218  // is that it handles Range requests properly, sets the MIME type, and
   219  // handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
   220  // and If-Range requests.
   221  //
   222  // If the response's Content-Type header is not set, ServeContent
   223  // first tries to deduce the type from name's file extension and,
   224  // if that fails, falls back to reading the first block of the content
   225  // and passing it to [DetectContentType].
   226  // The name is otherwise unused; in particular it can be empty and is
   227  // never sent in the response.
   228  //
   229  // If modtime is not the zero time or Unix epoch, ServeContent
   230  // includes it in a Last-Modified header in the response. If the
   231  // request includes an If-Modified-Since header, ServeContent uses
   232  // modtime to decide whether the content needs to be sent at all.
   233  //
   234  // The content's Seek method must work: ServeContent uses
   235  // a seek to the end of the content to determine its size.
   236  // Note that [*os.File] implements the [io.ReadSeeker] interface.
   237  //
   238  // If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
   239  // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
   240  //
   241  // If an error occurs when serving the request (for example, when
   242  // handling an invalid range request), ServeContent responds with an
   243  // error message. By default, ServeContent strips the Cache-Control,
   244  // Content-Encoding, ETag, and Last-Modified headers from error responses.
   245  // The GODEBUG setting httpservecontentkeepheaders=1 causes ServeContent
   246  // to preserve these headers.
   247  func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
   248  	sizeFunc := func() (int64, error) {
   249  		size, err := content.Seek(0, io.SeekEnd)
   250  		if err != nil {
   251  			return 0, errSeeker
   252  		}
   253  		_, err = content.Seek(0, io.SeekStart)
   254  		if err != nil {
   255  			return 0, errSeeker
   256  		}
   257  		return size, nil
   258  	}
   259  	serveContent(w, req, name, modtime, sizeFunc, content)
   260  }
   261  
   262  // errSeeker is returned by ServeContent's sizeFunc when the content
   263  // doesn't seek properly. The underlying Seeker's error text isn't
   264  // included in the sizeFunc reply so it's not sent over HTTP to end
   265  // users.
   266  var errSeeker = errors.New("seeker can't seek")
   267  
   268  // errNoOverlap is returned by serveContent's parseRange if first-byte-pos of
   269  // all of the byte-range-spec values is greater than the content size.
   270  var errNoOverlap = errors.New("invalid range: failed to overlap")
   271  
   272  // if name is empty, filename is unknown. (used for mime type, before sniffing)
   273  // if modtime.IsZero(), modtime is unknown.
   274  // content must be seeked to the beginning of the file.
   275  // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
   276  func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
   277  	setLastModified(w, modtime)
   278  	done, rangeReq := checkPreconditions(w, r, modtime)
   279  	if done {
   280  		return
   281  	}
   282  
   283  	code := StatusOK
   284  
   285  	// If Content-Type isn't set, use the file's extension to find it, but
   286  	// if the Content-Type is unset explicitly, do not sniff the type.
   287  	ctypes, haveType := w.Header()["Content-Type"]
   288  	var ctype string
   289  	if !haveType {
   290  		ctype = mime.TypeByExtension(filepath.Ext(name))
   291  		if ctype == "" {
   292  			// read a chunk to decide between utf-8 text and binary
   293  			var buf [internal.SniffLen]byte
   294  			n, _ := io.ReadFull(content, buf[:])
   295  			ctype = DetectContentType(buf[:n])
   296  			_, err := content.Seek(0, io.SeekStart) // rewind to output whole file
   297  			if err != nil {
   298  				serveError(w, "seeker can't seek", StatusInternalServerError)
   299  				return
   300  			}
   301  		}
   302  		w.Header().Set("Content-Type", ctype)
   303  	} else if len(ctypes) > 0 {
   304  		ctype = ctypes[0]
   305  	}
   306  
   307  	size, err := sizeFunc()
   308  	if err != nil {
   309  		serveError(w, err.Error(), StatusInternalServerError)
   310  		return
   311  	}
   312  	if size < 0 {
   313  		// Should never happen but just to be sure
   314  		serveError(w, "negative content size computed", StatusInternalServerError)
   315  		return
   316  	}
   317  
   318  	// handle Content-Range header.
   319  	sendSize := size
   320  	var sendContent io.Reader = content
   321  	ranges, err := parseRange(rangeReq, size)
   322  	switch err {
   323  	case nil:
   324  	case errNoOverlap:
   325  		if size == 0 {
   326  			// Some clients add a Range header to all requests to
   327  			// limit the size of the response. If the file is empty,
   328  			// ignore the range header and respond with a 200 rather
   329  			// than a 416.
   330  			ranges = nil
   331  			break
   332  		}
   333  		w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size))
   334  		fallthrough
   335  	default:
   336  		serveError(w, err.Error(), StatusRequestedRangeNotSatisfiable)
   337  		return
   338  	}
   339  
   340  	if sumRangesSize(ranges) > size {
   341  		// The total number of bytes in all the ranges
   342  		// is larger than the size of the file by
   343  		// itself, so this is probably an attack, or a
   344  		// dumb client. Ignore the range request.
   345  		ranges = nil
   346  	}
   347  	switch {
   348  	case len(ranges) == 1:
   349  		// RFC 7233, Section 4.1:
   350  		// "If a single part is being transferred, the server
   351  		// generating the 206 response MUST generate a
   352  		// Content-Range header field, describing what range
   353  		// of the selected representation is enclosed, and a
   354  		// payload consisting of the range.
   355  		// ...
   356  		// A server MUST NOT generate a multipart response to
   357  		// a request for a single range, since a client that
   358  		// does not request multiple parts might not support
   359  		// multipart responses."
   360  		ra := ranges[0]
   361  		if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
   362  			serveError(w, err.Error(), StatusRequestedRangeNotSatisfiable)
   363  			return
   364  		}
   365  		sendSize = ra.length
   366  		code = StatusPartialContent
   367  		w.Header().Set("Content-Range", ra.contentRange(size))
   368  	case len(ranges) > 1:
   369  		sendSize = rangesMIMESize(ranges, ctype, size)
   370  		code = StatusPartialContent
   371  
   372  		pr, pw := io.Pipe()
   373  		mw := multipart.NewWriter(pw)
   374  		w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
   375  		sendContent = pr
   376  		defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
   377  		go func() {
   378  			for _, ra := range ranges {
   379  				part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
   380  				if err != nil {
   381  					pw.CloseWithError(err)
   382  					return
   383  				}
   384  				if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
   385  					pw.CloseWithError(err)
   386  					return
   387  				}
   388  				if _, err := io.CopyN(part, content, ra.length); err != nil {
   389  					pw.CloseWithError(err)
   390  					return
   391  				}
   392  			}
   393  			mw.Close()
   394  			pw.Close()
   395  		}()
   396  	}
   397  
   398  	w.Header().Set("Accept-Ranges", "bytes")
   399  
   400  	// We should be able to unconditionally set the Content-Length here.
   401  	//
   402  	// However, there is a pattern observed in the wild that this breaks:
   403  	// The user wraps the ResponseWriter in one which gzips data written to it,
   404  	// and sets "Content-Encoding: gzip".
   405  	//
   406  	// The user shouldn't be doing this; the serveContent path here depends
   407  	// on serving seekable data with a known length. If you want to compress
   408  	// on the fly, then you shouldn't be using ServeFile/ServeContent, or
   409  	// you should compress the entire file up-front and provide a seekable
   410  	// view of the compressed data.
   411  	//
   412  	// However, since we've observed this pattern in the wild, and since
   413  	// setting Content-Length here breaks code that mostly-works today,
   414  	// skip setting Content-Length if the user set Content-Encoding.
   415  	//
   416  	// If this is a range request, always set Content-Length.
   417  	// If the user isn't changing the bytes sent in the ResponseWrite,
   418  	// the Content-Length will be correct.
   419  	// If the user is changing the bytes sent, then the range request wasn't
   420  	// going to work properly anyway and we aren't worse off.
   421  	//
   422  	// A possible future improvement on this might be to look at the type
   423  	// of the ResponseWriter, and always set Content-Length if it's one
   424  	// that we recognize.
   425  	if len(ranges) > 0 || w.Header().Get("Content-Encoding") == "" {
   426  		w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
   427  	}
   428  	w.WriteHeader(code)
   429  
   430  	if r.Method != "HEAD" {
   431  		io.CopyN(w, sendContent, sendSize)
   432  	}
   433  }
   434  
   435  // scanETag determines if a syntactically valid ETag is present at s. If so,
   436  // the ETag and remaining text after consuming ETag is returned. Otherwise,
   437  // it returns "", "".
   438  func scanETag(s string) (etag string, remain string) {
   439  	s = textproto.TrimString(s)
   440  	start := 0
   441  	if strings.HasPrefix(s, "W/") {
   442  		start = 2
   443  	}
   444  	if len(s[start:]) < 2 || s[start] != '"' {
   445  		return "", ""
   446  	}
   447  	// ETag is either W/"text" or "text".
   448  	// See RFC 7232 2.3.
   449  	for i := start + 1; i < len(s); i++ {
   450  		c := s[i]
   451  		switch {
   452  		// Character values allowed in ETags.
   453  		case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
   454  		case c == '"':
   455  			return s[:i+1], s[i+1:]
   456  		default:
   457  			return "", ""
   458  		}
   459  	}
   460  	return "", ""
   461  }
   462  
   463  // etagStrongMatch reports whether a and b match using strong ETag comparison.
   464  // Assumes a and b are valid ETags.
   465  func etagStrongMatch(a, b string) bool {
   466  	return a == b && a != "" && a[0] == '"'
   467  }
   468  
   469  // etagWeakMatch reports whether a and b match using weak ETag comparison.
   470  // Assumes a and b are valid ETags.
   471  func etagWeakMatch(a, b string) bool {
   472  	return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
   473  }
   474  
   475  // condResult is the result of an HTTP request precondition check.
   476  // See https://tools.ietf.org/html/rfc7232 section 3.
   477  type condResult int
   478  
   479  const (
   480  	condNone condResult = iota
   481  	condTrue
   482  	condFalse
   483  )
   484  
   485  func checkIfMatch(w ResponseWriter, r *Request) condResult {
   486  	im := r.Header.Get("If-Match")
   487  	if im == "" {
   488  		return condNone
   489  	}
   490  	for {
   491  		im = textproto.TrimString(im)
   492  		if len(im) == 0 {
   493  			break
   494  		}
   495  		if im[0] == ',' {
   496  			im = im[1:]
   497  			continue
   498  		}
   499  		if im[0] == '*' {
   500  			return condTrue
   501  		}
   502  		etag, remain := scanETag(im)
   503  		if etag == "" {
   504  			break
   505  		}
   506  		if etagStrongMatch(etag, w.Header().get("Etag")) {
   507  			return condTrue
   508  		}
   509  		im = remain
   510  	}
   511  
   512  	return condFalse
   513  }
   514  
   515  func checkIfUnmodifiedSince(r *Request, modtime time.Time) condResult {
   516  	ius := r.Header.Get("If-Unmodified-Since")
   517  	if ius == "" || isZeroTime(modtime) {
   518  		return condNone
   519  	}
   520  	t, err := ParseTime(ius)
   521  	if err != nil {
   522  		return condNone
   523  	}
   524  
   525  	// The Last-Modified header truncates sub-second precision so
   526  	// the modtime needs to be truncated too.
   527  	modtime = modtime.Truncate(time.Second)
   528  	if ret := modtime.Compare(t); ret <= 0 {
   529  		return condTrue
   530  	}
   531  	return condFalse
   532  }
   533  
   534  func checkIfNoneMatch(w ResponseWriter, r *Request) condResult {
   535  	inm := r.Header.get("If-None-Match")
   536  	if inm == "" {
   537  		return condNone
   538  	}
   539  	buf := inm
   540  	for {
   541  		buf = textproto.TrimString(buf)
   542  		if len(buf) == 0 {
   543  			break
   544  		}
   545  		if buf[0] == ',' {
   546  			buf = buf[1:]
   547  			continue
   548  		}
   549  		if buf[0] == '*' {
   550  			return condFalse
   551  		}
   552  		etag, remain := scanETag(buf)
   553  		if etag == "" {
   554  			break
   555  		}
   556  		if etagWeakMatch(etag, w.Header().get("Etag")) {
   557  			return condFalse
   558  		}
   559  		buf = remain
   560  	}
   561  	return condTrue
   562  }
   563  
   564  func checkIfModifiedSince(r *Request, modtime time.Time) condResult {
   565  	if r.Method != "GET" && r.Method != "HEAD" {
   566  		return condNone
   567  	}
   568  	ims := r.Header.Get("If-Modified-Since")
   569  	if ims == "" || isZeroTime(modtime) {
   570  		return condNone
   571  	}
   572  	t, err := ParseTime(ims)
   573  	if err != nil {
   574  		return condNone
   575  	}
   576  	// The Last-Modified header truncates sub-second precision so
   577  	// the modtime needs to be truncated too.
   578  	modtime = modtime.Truncate(time.Second)
   579  	if ret := modtime.Compare(t); ret <= 0 {
   580  		return condFalse
   581  	}
   582  	return condTrue
   583  }
   584  
   585  func checkIfRange(w ResponseWriter, r *Request, modtime time.Time) condResult {
   586  	if r.Method != "GET" && r.Method != "HEAD" {
   587  		return condNone
   588  	}
   589  	ir := r.Header.get("If-Range")
   590  	if ir == "" {
   591  		return condNone
   592  	}
   593  	etag, _ := scanETag(ir)
   594  	if etag != "" {
   595  		if etagStrongMatch(etag, w.Header().Get("Etag")) {
   596  			return condTrue
   597  		} else {
   598  			return condFalse
   599  		}
   600  	}
   601  	// The If-Range value is typically the ETag value, but it may also be
   602  	// the modtime date. See golang.org/issue/8367.
   603  	if modtime.IsZero() {
   604  		return condFalse
   605  	}
   606  	t, err := ParseTime(ir)
   607  	if err != nil {
   608  		return condFalse
   609  	}
   610  	if t.Unix() == modtime.Unix() {
   611  		return condTrue
   612  	}
   613  	return condFalse
   614  }
   615  
   616  var unixEpochTime = time.Unix(0, 0)
   617  
   618  // isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
   619  func isZeroTime(t time.Time) bool {
   620  	return t.IsZero() || t.Equal(unixEpochTime)
   621  }
   622  
   623  func setLastModified(w ResponseWriter, modtime time.Time) {
   624  	if !isZeroTime(modtime) {
   625  		w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
   626  	}
   627  }
   628  
   629  func writeNotModified(w ResponseWriter) {
   630  	// RFC 7232 section 4.1:
   631  	// a sender SHOULD NOT generate representation metadata other than the
   632  	// above listed fields unless said metadata exists for the purpose of
   633  	// guiding cache updates (e.g., Last-Modified might be useful if the
   634  	// response does not have an ETag field).
   635  	h := w.Header()
   636  	delete(h, "Content-Type")
   637  	delete(h, "Content-Length")
   638  	delete(h, "Content-Encoding")
   639  	if h.Get("Etag") != "" {
   640  		delete(h, "Last-Modified")
   641  	}
   642  	w.WriteHeader(StatusNotModified)
   643  }
   644  
   645  // checkPreconditions evaluates request preconditions and reports whether a precondition
   646  // resulted in sending StatusNotModified or StatusPreconditionFailed.
   647  func checkPreconditions(w ResponseWriter, r *Request, modtime time.Time) (done bool, rangeHeader string) {
   648  	// This function carefully follows RFC 7232 section 6.
   649  	ch := checkIfMatch(w, r)
   650  	if ch == condNone {
   651  		ch = checkIfUnmodifiedSince(r, modtime)
   652  	}
   653  	if ch == condFalse {
   654  		w.WriteHeader(StatusPreconditionFailed)
   655  		return true, ""
   656  	}
   657  	switch checkIfNoneMatch(w, r) {
   658  	case condFalse:
   659  		if r.Method == "GET" || r.Method == "HEAD" {
   660  			writeNotModified(w)
   661  			return true, ""
   662  		} else {
   663  			w.WriteHeader(StatusPreconditionFailed)
   664  			return true, ""
   665  		}
   666  	case condNone:
   667  		if checkIfModifiedSince(r, modtime) == condFalse {
   668  			writeNotModified(w)
   669  			return true, ""
   670  		}
   671  	}
   672  
   673  	rangeHeader = r.Header.get("Range")
   674  	if rangeHeader != "" && checkIfRange(w, r, modtime) == condFalse {
   675  		rangeHeader = ""
   676  	}
   677  	return false, rangeHeader
   678  }
   679  
   680  // name is '/'-separated, not filepath.Separator.
   681  func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
   682  	const indexPage = "/index.html"
   683  
   684  	// redirect .../index.html to .../
   685  	// can't use Redirect() because that would make the path absolute,
   686  	// which would be a problem running under StripPrefix
   687  	if strings.HasSuffix(r.URL.Path, indexPage) {
   688  		localRedirect(w, r, "./")
   689  		return
   690  	}
   691  
   692  	f, err := fs.Open(name)
   693  	if err != nil {
   694  		msg, code := toHTTPError(err)
   695  		serveError(w, msg, code)
   696  		return
   697  	}
   698  	defer f.Close()
   699  
   700  	d, err := f.Stat()
   701  	if err != nil {
   702  		msg, code := toHTTPError(err)
   703  		serveError(w, msg, code)
   704  		return
   705  	}
   706  
   707  	if redirect {
   708  		// redirect to canonical path: / at end of directory url
   709  		// r.URL.Path always begins with /
   710  		url := r.URL.Path
   711  		if d.IsDir() {
   712  			if url[len(url)-1] != '/' {
   713  				localRedirect(w, r, path.Base(url)+"/")
   714  				return
   715  			}
   716  		} else if url[len(url)-1] == '/' {
   717  			base := path.Base(url)
   718  			if base == "/" || base == "." {
   719  				// The FileSystem maps a path like "/" or "/./" to a file instead of a directory.
   720  				msg := "http: attempting to traverse a non-directory"
   721  				serveError(w, msg, StatusInternalServerError)
   722  				return
   723  			}
   724  			localRedirect(w, r, "../"+base)
   725  			return
   726  		}
   727  	}
   728  
   729  	if d.IsDir() {
   730  		url := r.URL.Path
   731  		// redirect if the directory name doesn't end in a slash
   732  		if url == "" || url[len(url)-1] != '/' {
   733  			localRedirect(w, r, path.Base(url)+"/")
   734  			return
   735  		}
   736  
   737  		// use contents of index.html for directory, if present
   738  		index := strings.TrimSuffix(name, "/") + indexPage
   739  		ff, err := fs.Open(index)
   740  		if err == nil {
   741  			defer ff.Close()
   742  			dd, err := ff.Stat()
   743  			if err == nil {
   744  				d = dd
   745  				f = ff
   746  			}
   747  		}
   748  	}
   749  
   750  	// Still a directory? (we didn't find an index.html file)
   751  	if d.IsDir() {
   752  		if checkIfModifiedSince(r, d.ModTime()) == condFalse {
   753  			writeNotModified(w)
   754  			return
   755  		}
   756  		setLastModified(w, d.ModTime())
   757  		dirList(w, r, f)
   758  		return
   759  	}
   760  
   761  	// serveContent will check modification time
   762  	sizeFunc := func() (int64, error) { return d.Size(), nil }
   763  	serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
   764  }
   765  
   766  // toHTTPError returns a non-specific HTTP error message and status code
   767  // for a given non-nil error value. It's important that toHTTPError does not
   768  // actually return err.Error(), since msg and httpStatus are returned to users,
   769  // and historically Go's ServeContent always returned just "404 Not Found" for
   770  // all errors. We don't want to start leaking information in error messages.
   771  func toHTTPError(err error) (msg string, httpStatus int) {
   772  	if errors.Is(err, fs.ErrNotExist) {
   773  		return "404 page not found", StatusNotFound
   774  	}
   775  	if errors.Is(err, fs.ErrPermission) {
   776  		return "403 Forbidden", StatusForbidden
   777  	}
   778  	if errors.Is(err, errInvalidUnsafePath) {
   779  		return "404 page not found", StatusNotFound
   780  	}
   781  	// Default:
   782  	return "500 Internal Server Error", StatusInternalServerError
   783  }
   784  
   785  // localRedirect gives a Moved Permanently response.
   786  // It does not convert relative paths to absolute paths like Redirect does.
   787  func localRedirect(w ResponseWriter, r *Request, newPath string) {
   788  	// There is no reliable way for us to redirect correctly when the path has
   789  	// escaped slashes, since StripPrefix might be in use. Just return 404.
   790  	if p := r.URL.EscapedPath(); strings.Contains(p, "%2f") || strings.Contains(p, "%2F") {
   791  		NotFound(w, r)
   792  		return
   793  	}
   794  	if q := r.URL.RawQuery; q != "" {
   795  		newPath += "?" + q
   796  	}
   797  	w.Header().Set("Location", newPath)
   798  	w.WriteHeader(StatusMovedPermanently)
   799  }
   800  
   801  // ServeFile replies to the request with the contents of the named
   802  // file or directory.
   803  //
   804  // If the provided file or directory name is a relative path, it is
   805  // interpreted relative to the current directory and may ascend to
   806  // parent directories. If the provided name is constructed from user
   807  // input, it should be sanitized before calling [ServeFile].
   808  //
   809  // As a precaution, ServeFile will reject requests where r.URL.Path
   810  // contains a ".." path element; this protects against callers who
   811  // might unsafely use [filepath.Join] on r.URL.Path without sanitizing
   812  // it and then use that filepath.Join result as the name argument.
   813  //
   814  // As another special case, ServeFile redirects any request where r.URL.Path
   815  // ends in "/index.html" to the same path, without the final
   816  // "index.html". To avoid such redirects either modify the path or
   817  // use [ServeContent].
   818  //
   819  // Outside of those two special cases, ServeFile does not use
   820  // r.URL.Path for selecting the file or directory to serve; only the
   821  // file or directory provided in the name argument is used.
   822  func ServeFile(w ResponseWriter, r *Request, name string) {
   823  	if containsDotDot(r.URL.Path) {
   824  		// Too many programs use r.URL.Path to construct the argument to
   825  		// serveFile. Reject the request under the assumption that happened
   826  		// here and ".." may not be wanted.
   827  		// Note that name might not contain "..", for example if code (still
   828  		// incorrectly) used filepath.Join(myDir, r.URL.Path).
   829  		serveError(w, "invalid URL path", StatusBadRequest)
   830  		return
   831  	}
   832  	dir, file := filepath.Split(name)
   833  	serveFile(w, r, Dir(dir), file, false)
   834  }
   835  
   836  // ServeFileFS replies to the request with the contents
   837  // of the named file or directory from the file system fsys.
   838  // The files provided by fsys must implement [io.Seeker].
   839  //
   840  // If the provided name is constructed from user input, it should be
   841  // sanitized before calling [ServeFileFS].
   842  //
   843  // As a precaution, ServeFileFS will reject requests where r.URL.Path
   844  // contains a ".." path element; this protects against callers who
   845  // might unsafely use [filepath.Join] on r.URL.Path without sanitizing
   846  // it and then use that filepath.Join result as the name argument.
   847  //
   848  // As another special case, ServeFileFS redirects any request where r.URL.Path
   849  // ends in "/index.html" to the same path, without the final
   850  // "index.html". To avoid such redirects either modify the path or
   851  // use [ServeContent].
   852  //
   853  // Outside of those two special cases, ServeFileFS does not use
   854  // r.URL.Path for selecting the file or directory to serve; only the
   855  // file or directory provided in the name argument is used.
   856  func ServeFileFS(w ResponseWriter, r *Request, fsys fs.FS, name string) {
   857  	if containsDotDot(r.URL.Path) {
   858  		// Too many programs use r.URL.Path to construct the argument to
   859  		// serveFile. Reject the request under the assumption that happened
   860  		// here and ".." may not be wanted.
   861  		// Note that name might not contain "..", for example if code (still
   862  		// incorrectly) used filepath.Join(myDir, r.URL.Path).
   863  		serveError(w, "invalid URL path", StatusBadRequest)
   864  		return
   865  	}
   866  	serveFile(w, r, FS(fsys), name, false)
   867  }
   868  
   869  func containsDotDot(v string) bool {
   870  	if !strings.Contains(v, "..") {
   871  		return false
   872  	}
   873  	for ent := range strings.FieldsFuncSeq(v, isSlashRune) {
   874  		if ent == ".." {
   875  			return true
   876  		}
   877  	}
   878  	return false
   879  }
   880  
   881  func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
   882  
   883  type fileHandler struct {
   884  	root FileSystem
   885  }
   886  
   887  type ioFS struct {
   888  	fsys fs.FS
   889  }
   890  
   891  type ioFile struct {
   892  	file fs.File
   893  }
   894  
   895  func (f ioFS) Open(name string) (File, error) {
   896  	if name == "/" {
   897  		name = "."
   898  	} else {
   899  		name = strings.TrimPrefix(name, "/")
   900  	}
   901  	file, err := f.fsys.Open(name)
   902  	if err != nil {
   903  		return nil, mapOpenError(err, name, '/', func(path string) (fs.FileInfo, error) {
   904  			return fs.Stat(f.fsys, path)
   905  		})
   906  	}
   907  	return ioFile{file}, nil
   908  }
   909  
   910  func (f ioFile) Close() error               { return f.file.Close() }
   911  func (f ioFile) Read(b []byte) (int, error) { return f.file.Read(b) }
   912  func (f ioFile) Stat() (fs.FileInfo, error) { return f.file.Stat() }
   913  
   914  var errMissingSeek = errors.New("io.File missing Seek method")
   915  var errMissingReadDir = errors.New("io.File directory missing ReadDir method")
   916  
   917  func (f ioFile) Seek(offset int64, whence int) (int64, error) {
   918  	s, ok := f.file.(io.Seeker)
   919  	if !ok {
   920  		return 0, errMissingSeek
   921  	}
   922  	return s.Seek(offset, whence)
   923  }
   924  
   925  func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) {
   926  	d, ok := f.file.(fs.ReadDirFile)
   927  	if !ok {
   928  		return nil, errMissingReadDir
   929  	}
   930  	return d.ReadDir(count)
   931  }
   932  
   933  func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) {
   934  	d, ok := f.file.(fs.ReadDirFile)
   935  	if !ok {
   936  		return nil, errMissingReadDir
   937  	}
   938  	var list []fs.FileInfo
   939  	for {
   940  		dirs, err := d.ReadDir(count - len(list))
   941  		for _, dir := range dirs {
   942  			info, err := dir.Info()
   943  			if err != nil {
   944  				// Pretend it doesn't exist, like (*os.File).Readdir does.
   945  				continue
   946  			}
   947  			list = append(list, info)
   948  		}
   949  		if err != nil {
   950  			return list, err
   951  		}
   952  		if count < 0 || len(list) >= count {
   953  			break
   954  		}
   955  	}
   956  	return list, nil
   957  }
   958  
   959  // FS converts fsys to a [FileSystem] implementation,
   960  // for use with [FileServer] and [NewFileTransport].
   961  // The files provided by fsys must implement [io.Seeker].
   962  //
   963  // Most users should use [FileServerFS] instead.
   964  func FS(fsys fs.FS) FileSystem {
   965  	return ioFS{fsys}
   966  }
   967  
   968  // FileServer returns a handler that serves HTTP requests
   969  // with the contents of the file system rooted at root.
   970  // Most users should use [FileServerFS] instead.
   971  //
   972  // As a special case, the returned file server redirects any request
   973  // ending in "/index.html" to the same path, without the final
   974  // "index.html".
   975  func FileServer(root FileSystem) Handler {
   976  	return &fileHandler{root}
   977  }
   978  
   979  // FileServerFS returns a handler that serves HTTP requests
   980  // with the contents of the file system fsys.
   981  // The files provided by fsys must implement [io.Seeker].
   982  //
   983  // As a special case, the returned file server redirects any request
   984  // ending in "/index.html" to the same path, without the final
   985  // "index.html".
   986  //
   987  // FileServer serves all files contained within the [fs.FS].
   988  // The examples demonstrate safely serving a local directory
   989  // while blocking symlinks that lead outside the directory,
   990  // and serving a local directory while hiding files that
   991  // start with a dot such as ".git" and ".htpassword".
   992  func FileServerFS(fsys fs.FS) Handler {
   993  	return FileServer(FS(fsys))
   994  }
   995  
   996  func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
   997  	upath := r.URL.Path
   998  	if !strings.HasPrefix(upath, "/") {
   999  		upath = "/" + upath
  1000  		r.URL.Path = upath
  1001  	}
  1002  	serveFile(w, r, f.root, path.Clean(upath), true)
  1003  }
  1004  
  1005  // httpRange specifies the byte range to be sent to the client.
  1006  type httpRange struct {
  1007  	start, length int64
  1008  }
  1009  
  1010  func (r httpRange) contentRange(size int64) string {
  1011  	return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
  1012  }
  1013  
  1014  func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
  1015  	return textproto.MIMEHeader{
  1016  		"Content-Range": {r.contentRange(size)},
  1017  		"Content-Type":  {contentType},
  1018  	}
  1019  }
  1020  
  1021  // parseRange parses a Range header string as per RFC 7233.
  1022  // errNoOverlap is returned if none of the ranges overlap.
  1023  func parseRange(s string, size int64) ([]httpRange, error) {
  1024  	if s == "" {
  1025  		return nil, nil // header not present
  1026  	}
  1027  	const b = "bytes="
  1028  	if len(s) < len(b) || !ascii.EqualFold(s[:len(b)], b) {
  1029  		return nil, errors.New("invalid range")
  1030  	}
  1031  	var ranges []httpRange
  1032  	noOverlap := false
  1033  	for ra := range strings.SplitSeq(s[len(b):], ",") {
  1034  		ra = textproto.TrimString(ra)
  1035  		if ra == "" {
  1036  			continue
  1037  		}
  1038  		start, end, ok := strings.Cut(ra, "-")
  1039  		if !ok {
  1040  			return nil, errors.New("invalid range")
  1041  		}
  1042  		start, end = textproto.TrimString(start), textproto.TrimString(end)
  1043  		var r httpRange
  1044  		if start == "" {
  1045  			// If no start is specified, end specifies the
  1046  			// range start relative to the end of the file,
  1047  			// and we are dealing with <suffix-length>
  1048  			// which has to be a non-negative integer as per
  1049  			// RFC 7233 Section 2.1 "Byte-Ranges".
  1050  			if end == "" || end[0] == '-' {
  1051  				return nil, errors.New("invalid range")
  1052  			}
  1053  			i, err := strconv.ParseInt(end, 10, 64)
  1054  			if i < 0 || err != nil {
  1055  				return nil, errors.New("invalid range")
  1056  			}
  1057  			if i > size {
  1058  				i = size
  1059  			}
  1060  			r.start = size - i
  1061  			r.length = size - r.start
  1062  		} else {
  1063  			i, err := strconv.ParseInt(start, 10, 64)
  1064  			if err != nil || i < 0 {
  1065  				return nil, errors.New("invalid range")
  1066  			}
  1067  			if i >= size {
  1068  				// If the range begins after the size of the content,
  1069  				// then it does not overlap.
  1070  				noOverlap = true
  1071  				continue
  1072  			}
  1073  			r.start = i
  1074  			if end == "" {
  1075  				// If no end is specified, range extends to end of the file.
  1076  				r.length = size - r.start
  1077  			} else {
  1078  				i, err := strconv.ParseInt(end, 10, 64)
  1079  				if err != nil || r.start > i {
  1080  					return nil, errors.New("invalid range")
  1081  				}
  1082  				if i >= size {
  1083  					i = size - 1
  1084  				}
  1085  				r.length = i - r.start + 1
  1086  			}
  1087  		}
  1088  		ranges = append(ranges, r)
  1089  	}
  1090  	if noOverlap && len(ranges) == 0 {
  1091  		// The specified ranges did not overlap with the content.
  1092  		return nil, errNoOverlap
  1093  	}
  1094  	return ranges, nil
  1095  }
  1096  
  1097  // countingWriter counts how many bytes have been written to it.
  1098  type countingWriter int64
  1099  
  1100  func (w *countingWriter) Write(p []byte) (n int, err error) {
  1101  	*w += countingWriter(len(p))
  1102  	return len(p), nil
  1103  }
  1104  
  1105  // rangesMIMESize returns the number of bytes it takes to encode the
  1106  // provided ranges as a multipart response.
  1107  func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
  1108  	var w countingWriter
  1109  	mw := multipart.NewWriter(&w)
  1110  	for _, ra := range ranges {
  1111  		mw.CreatePart(ra.mimeHeader(contentType, contentSize))
  1112  		encSize += ra.length
  1113  	}
  1114  	mw.Close()
  1115  	encSize += int64(w)
  1116  	return
  1117  }
  1118  
  1119  func sumRangesSize(ranges []httpRange) (size int64) {
  1120  	for _, ra := range ranges {
  1121  		size += ra.length
  1122  	}
  1123  	return
  1124  }
  1125  

View as plain text