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

View as plain text