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
7package http
8
9import (
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 ".".
43type Dir string
44
45// mapDirOpenError 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 Issue 18984.
48func mapDirOpenError(originalErr error, name string) error {
49	if os.IsNotExist(originalErr) || os.IsPermission(originalErr) {
50		return originalErr
51	}
52
53	parts := strings.Split(name, string(filepath.Separator))
54	for i := range parts {
55		if parts[i] == "" {
56			continue
57		}
58		fi, err := os.Stat(strings.Join(parts[:i+1], string(filepath.Separator)))
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.
71func (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, mapDirOpenError(err, fullName)
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.
94type 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.
102type 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
110type anyDirs interface {
111	len() int
112	name(i int) string
113	isDir(i int) bool
114}
115
116type fileInfoDirs []fs.FileInfo
117
118func (d fileInfoDirs) len() int          { return len(d) }
119func (d fileInfoDirs) isDir(i int) bool  { return d[i].IsDir() }
120func (d fileInfoDirs) name(i int) string { return d[i].Name() }
121
122type dirEntryDirs []fs.DirEntry
123
124func (d dirEntryDirs) len() int          { return len(d) }
125func (d dirEntryDirs) isDir(i int) bool  { return d[i].IsDir() }
126func (d dirEntryDirs) name(i int) string { return d[i].Name() }
127
128func 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.
192func 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.
211var 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.
215var 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.
221func 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 "", "".
344func 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.
371func 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.
377func 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.
383type condResult int
384
385const (
386	condNone condResult = iota
387	condTrue
388	condFalse
389)
390
391func 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
421func 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
440func 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
470func 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
491func 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
522var unixEpochTime = time.Unix(0, 0)
523
524// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
525func isZeroTime(t time.Time) bool {
526	return t.IsZero() || t.Equal(unixEpochTime)
527}
528
529func setLastModified(w ResponseWriter, modtime time.Time) {
530	if !isZeroTime(modtime) {
531		w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
532	}
533}
534
535func 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.
552func 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.
586func 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.
672func toHTTPError(err error) (msg string, httpStatus int) {
673	if os.IsNotExist(err) {
674		return "404 page not found", StatusNotFound
675	}
676	if os.IsPermission(err) {
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.
685func 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.
714func 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
728func 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
740func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
741
742type fileHandler struct {
743	root FileSystem
744}
745
746type ioFS struct {
747	fsys fs.FS
748}
749
750type ioFile struct {
751	file fs.File
752}
753
754func (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, err
763	}
764	return ioFile{file}, nil
765}
766
767func (f ioFile) Close() error               { return f.file.Close() }
768func (f ioFile) Read(b []byte) (int, error) { return f.file.Read(b) }
769func (f ioFile) Stat() (fs.FileInfo, error) { return f.file.Stat() }
770
771var errMissingSeek = errors.New("io.File missing Seek method")
772var errMissingReadDir = errors.New("io.File directory missing ReadDir method")
773
774func (f ioFile) Seek(offset int64, whence int) (int64, error) {
775	s, ok := f.file.(io.Seeker)
776	if !ok {
777		return 0, errMissingSeek
778	}
779	return s.Seek(offset, whence)
780}
781
782func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) {
783	d, ok := f.file.(fs.ReadDirFile)
784	if !ok {
785		return nil, errMissingReadDir
786	}
787	return d.ReadDir(count)
788}
789
790func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) {
791	d, ok := f.file.(fs.ReadDirFile)
792	if !ok {
793		return nil, errMissingReadDir
794	}
795	var list []fs.FileInfo
796	for {
797		dirs, err := d.ReadDir(count - len(list))
798		for _, dir := range dirs {
799			info, err := dir.Info()
800			if err != nil {
801				// Pretend it doesn't exist, like (*os.File).Readdir does.
802				continue
803			}
804			list = append(list, info)
805		}
806		if err != nil {
807			return list, err
808		}
809		if count < 0 || len(list) >= count {
810			break
811		}
812	}
813	return list, nil
814}
815
816// FS converts fsys to a FileSystem implementation,
817// for use with FileServer and NewFileTransport.
818func FS(fsys fs.FS) FileSystem {
819	return ioFS{fsys}
820}
821
822// FileServer returns a handler that serves HTTP requests
823// with the contents of the file system rooted at root.
824//
825// As a special case, the returned file server redirects any request
826// ending in "/index.html" to the same path, without the final
827// "index.html".
828//
829// To use the operating system's file system implementation,
830// use http.Dir:
831//
832//     http.Handle("/", http.FileServer(http.Dir("/tmp")))
833//
834// To use an fs.FS implementation, use http.FS to convert it:
835//
836//	http.Handle("/", http.FileServer(http.FS(fsys)))
837//
838func FileServer(root FileSystem) Handler {
839	return &fileHandler{root}
840}
841
842func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
843	upath := r.URL.Path
844	if !strings.HasPrefix(upath, "/") {
845		upath = "/" + upath
846		r.URL.Path = upath
847	}
848	serveFile(w, r, f.root, path.Clean(upath), true)
849}
850
851// httpRange specifies the byte range to be sent to the client.
852type httpRange struct {
853	start, length int64
854}
855
856func (r httpRange) contentRange(size int64) string {
857	return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
858}
859
860func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
861	return textproto.MIMEHeader{
862		"Content-Range": {r.contentRange(size)},
863		"Content-Type":  {contentType},
864	}
865}
866
867// parseRange parses a Range header string as per RFC 7233.
868// errNoOverlap is returned if none of the ranges overlap.
869func parseRange(s string, size int64) ([]httpRange, error) {
870	if s == "" {
871		return nil, nil // header not present
872	}
873	const b = "bytes="
874	if !strings.HasPrefix(s, b) {
875		return nil, errors.New("invalid range")
876	}
877	var ranges []httpRange
878	noOverlap := false
879	for _, ra := range strings.Split(s[len(b):], ",") {
880		ra = textproto.TrimString(ra)
881		if ra == "" {
882			continue
883		}
884		i := strings.Index(ra, "-")
885		if i < 0 {
886			return nil, errors.New("invalid range")
887		}
888		start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])
889		var r httpRange
890		if start == "" {
891			// If no start is specified, end specifies the
892			// range start relative to the end of the file,
893			// and we are dealing with <suffix-length>
894			// which has to be a non-negative integer as per
895			// RFC 7233 Section 2.1 "Byte-Ranges".
896			if end == "" || end[0] == '-' {
897				return nil, errors.New("invalid range")
898			}
899			i, err := strconv.ParseInt(end, 10, 64)
900			if i < 0 || err != nil {
901				return nil, errors.New("invalid range")
902			}
903			if i > size {
904				i = size
905			}
906			r.start = size - i
907			r.length = size - r.start
908		} else {
909			i, err := strconv.ParseInt(start, 10, 64)
910			if err != nil || i < 0 {
911				return nil, errors.New("invalid range")
912			}
913			if i >= size {
914				// If the range begins after the size of the content,
915				// then it does not overlap.
916				noOverlap = true
917				continue
918			}
919			r.start = i
920			if end == "" {
921				// If no end is specified, range extends to end of the file.
922				r.length = size - r.start
923			} else {
924				i, err := strconv.ParseInt(end, 10, 64)
925				if err != nil || r.start > i {
926					return nil, errors.New("invalid range")
927				}
928				if i >= size {
929					i = size - 1
930				}
931				r.length = i - r.start + 1
932			}
933		}
934		ranges = append(ranges, r)
935	}
936	if noOverlap && len(ranges) == 0 {
937		// The specified ranges did not overlap with the content.
938		return nil, errNoOverlap
939	}
940	return ranges, nil
941}
942
943// countingWriter counts how many bytes have been written to it.
944type countingWriter int64
945
946func (w *countingWriter) Write(p []byte) (n int, err error) {
947	*w += countingWriter(len(p))
948	return len(p), nil
949}
950
951// rangesMIMESize returns the number of bytes it takes to encode the
952// provided ranges as a multipart response.
953func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
954	var w countingWriter
955	mw := multipart.NewWriter(&w)
956	for _, ra := range ranges {
957		mw.CreatePart(ra.mimeHeader(contentType, contentSize))
958		encSize += ra.length
959	}
960	mw.Close()
961	encSize += int64(w)
962	return
963}
964
965func sumRangesSize(ranges []httpRange) (size int64) {
966	for _, ra := range ranges {
967		size += ra.length
968	}
969	return
970}
971