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