// Copyright 2009 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.// HTTP file system request handlerpackage httpimport ()// A Dir implements FileSystem using the native file system restricted to a// specific directory tree.//// While the FileSystem.Open method takes '/'-separated paths, a Dir's string// value is a filename on the native file system, not a URL, so it is separated// by filepath.Separator, which isn't necessarily '/'.//// Note that Dir could expose sensitive files and directories. Dir will follow// symlinks pointing out of the directory tree, which can be especially dangerous// if serving from a directory in which users are able to create arbitrary symlinks.// Dir will also allow access to files and directories starting with a period,// which could expose sensitive directories like .git or sensitive files like// .htpasswd. To exclude files with a leading period, remove the files/directories// from the server or create a custom FileSystem implementation.//// An empty Dir is treated as ".".typeDirstring// mapOpenError maps the provided non-nil error from opening name// to a possibly better non-nil error. In particular, it turns OS-specific errors// about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552.func ( error, string, rune, func(string) (fs.FileInfo, error)) error {iferrors.Is(, fs.ErrNotExist) || errors.Is(, fs.ErrPermission) {return } := strings.Split(, string())for := range {if [] == "" {continue } , := (strings.Join([:+1], string()))if != nil {return }if !.IsDir() {returnfs.ErrNotExist } }return}// Open implements FileSystem using os.Open, opening files for reading rooted// and relative to the directory d.func ( Dir) ( string) (File, error) { , := safefilepath.FromFS(path.Clean("/" + ))if != nil {returnnil, errors.New("http: invalid or unsafe file path") } := string()if == "" { = "." } := filepath.Join(, ) , := os.Open()if != nil {returnnil, mapOpenError(, , filepath.Separator, os.Stat) }return , nil}// A FileSystem implements access to a collection of named files.// The elements in a file path are separated by slash ('/', U+002F)// characters, regardless of host operating system convention.// See the FileServer function to convert a FileSystem to a Handler.//// This interface predates the fs.FS interface, which can be used instead:// the FS adapter function converts an fs.FS to a FileSystem.typeFileSysteminterface {Open(name string) (File, error)}// A File is returned by a FileSystem's Open method and can be// served by the FileServer implementation.//// The methods should behave the same as those on an *os.File.typeFileinterface {io.Closerio.Readerio.SeekerReaddir(count int) ([]fs.FileInfo, error)Stat() (fs.FileInfo, error)}typeanyDirsinterface {len() intname(i int) stringisDir(i int) bool}typefileInfoDirs []fs.FileInfofunc ( fileInfoDirs) () int { returnlen() }func ( fileInfoDirs) ( int) bool { return [].IsDir() }func ( fileInfoDirs) ( int) string { return [].Name() }typedirEntryDirs []fs.DirEntryfunc ( dirEntryDirs) () int { returnlen() }func ( dirEntryDirs) ( int) bool { return [].IsDir() }func ( dirEntryDirs) ( int) string { return [].Name() }func ( ResponseWriter, *Request, File) {// Prefer to use ReadDir instead of Readdir, // because the former doesn't require calling // Stat on every entry of a directory on Unix.varanyDirsvarerrorif , := .(fs.ReadDirFile); {vardirEntryDirs , = .ReadDir(-1) = } else {varfileInfoDirs , = .Readdir(-1) = }if != nil {logf(, "http: error reading directory: %v", )Error(, "Error reading directory", StatusInternalServerError)return }sort.Slice(, func(, int) bool { return .name() < .name() }) .Header().Set("Content-Type", "text/html; charset=utf-8")fmt.Fprintf(, "<pre>\n")for , := 0, .len(); < ; ++ { := .name()if .isDir() { += "/" }// name may contain '?' or '#', which must be escaped to remain // part of the URL path, and not indicate the start of a query // string or fragment. := url.URL{Path: }fmt.Fprintf(, "<a href=\"%s\">%s</a>\n", .String(), htmlReplacer.Replace()) }fmt.Fprintf(, "</pre>\n")}// ServeContent replies to the request using the content in the// provided ReadSeeker. The main benefit of ServeContent over io.Copy// is that it handles Range requests properly, sets the MIME type, and// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,// and If-Range requests.//// If the response's Content-Type header is not set, ServeContent// first tries to deduce the type from name's file extension and,// if that fails, falls back to reading the first block of the content// and passing it to DetectContentType.// The name is otherwise unused; in particular it can be empty and is// never sent in the response.//// If modtime is not the zero time or Unix epoch, ServeContent// includes it in a Last-Modified header in the response. If the// request includes an If-Modified-Since header, ServeContent uses// modtime to decide whether the content needs to be sent at all.//// The content's Seek method must work: ServeContent uses// a seek to the end of the content to determine its size.//// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.//// Note that *os.File implements the io.ReadSeeker interface.func ( ResponseWriter, *Request, string, time.Time, io.ReadSeeker) { := func() (int64, error) { , := .Seek(0, io.SeekEnd)if != nil {return0, errSeeker } _, = .Seek(0, io.SeekStart)if != nil {return0, errSeeker }return , nil }serveContent(, , , , , )}// errSeeker is returned by ServeContent's sizeFunc when the content// doesn't seek properly. The underlying Seeker's error text isn't// included in the sizeFunc reply so it's not sent over HTTP to end// users.varerrSeeker = errors.New("seeker can't seek")// errNoOverlap is returned by serveContent's parseRange if first-byte-pos of// all of the byte-range-spec values is greater than the content size.varerrNoOverlap = errors.New("invalid range: failed to overlap")// if name is empty, filename is unknown. (used for mime type, before sniffing)// if modtime.IsZero(), modtime is unknown.// content must be seeked to the beginning of the file.// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.func ( ResponseWriter, *Request, string, time.Time, func() (int64, error), io.ReadSeeker) {setLastModified(, ) , := checkPreconditions(, , )if {return } := StatusOK// If Content-Type isn't set, use the file's extension to find it, but // if the Content-Type is unset explicitly, do not sniff the type. , := .Header()["Content-Type"]varstringif ! { = mime.TypeByExtension(filepath.Ext())if == "" {// read a chunk to decide between utf-8 text and binaryvar [sniffLen]byte , := io.ReadFull(, [:]) = DetectContentType([:]) , := .Seek(0, io.SeekStart) // rewind to output whole fileif != nil {Error(, "seeker can't seek", StatusInternalServerError)return } } .Header().Set("Content-Type", ) } elseiflen() > 0 { = [0] } , := ()if != nil {Error(, .Error(), StatusInternalServerError)return }if < 0 {// Should never happen but just to be sureError(, "negative content size computed", StatusInternalServerError)return }// handle Content-Range header. := vario.Reader = , := parseRange(, )switch {casenil:caseerrNoOverlap:if == 0 {// Some clients add a Range header to all requests to // limit the size of the response. If the file is empty, // ignore the range header and respond with a 200 rather // than a 416. = nilbreak } .Header().Set("Content-Range", fmt.Sprintf("bytes */%d", ))fallthroughdefault:Error(, .Error(), StatusRequestedRangeNotSatisfiable)return }ifsumRangesSize() > {// The total number of bytes in all the ranges // is larger than the size of the file by // itself, so this is probably an attack, or a // dumb client. Ignore the range request. = nil }switch {caselen() == 1:// RFC 7233, Section 4.1: // "If a single part is being transferred, the server // generating the 206 response MUST generate a // Content-Range header field, describing what range // of the selected representation is enclosed, and a // payload consisting of the range. // ... // A server MUST NOT generate a multipart response to // a request for a single range, since a client that // does not request multiple parts might not support // multipart responses." := [0]if , := .Seek(.start, io.SeekStart); != nil {Error(, .Error(), StatusRequestedRangeNotSatisfiable)return } = .length = StatusPartialContent .Header().Set("Content-Range", .contentRange())caselen() > 1: = rangesMIMESize(, , ) = StatusPartialContent , := io.Pipe() := multipart.NewWriter() .Header().Set("Content-Type", "multipart/byteranges; boundary="+.Boundary()) = defer .Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.gofunc() {for , := range { , := .CreatePart(.mimeHeader(, ))if != nil { .CloseWithError()return }if , := .Seek(.start, io.SeekStart); != nil { .CloseWithError()return }if , := io.CopyN(, , .length); != nil { .CloseWithError()return } } .Close() .Close() }() } .Header().Set("Accept-Ranges", "bytes")if .Header().Get("Content-Encoding") == "" { .Header().Set("Content-Length", strconv.FormatInt(, 10)) } .WriteHeader()if .Method != "HEAD" {io.CopyN(, , ) }}// scanETag determines if a syntactically valid ETag is present at s. If so,// the ETag and remaining text after consuming ETag is returned. Otherwise,// it returns "", "".func ( string) ( string, string) { = textproto.TrimString() := 0ifstrings.HasPrefix(, "W/") { = 2 }iflen([:]) < 2 || [] != '"' {return"", "" }// ETag is either W/"text" or "text". // See RFC 7232 2.3.for := + 1; < len(); ++ { := []switch {// Character values allowed in ETags.case == 0x21 || >= 0x23 && <= 0x7E || >= 0x80:case == '"':return [:+1], [+1:]default:return"", "" } }return"", ""}// etagStrongMatch reports whether a and b match using strong ETag comparison.// Assumes a and b are valid ETags.func (, string) bool {return == && != "" && [0] == '"'}// etagWeakMatch reports whether a and b match using weak ETag comparison.// Assumes a and b are valid ETags.func (, string) bool {returnstrings.TrimPrefix(, "W/") == strings.TrimPrefix(, "W/")}// condResult is the result of an HTTP request precondition check.// See https://tools.ietf.org/html/rfc7232 section 3.typecondResultintconst (condNonecondResult = iotacondTruecondFalse)func ( ResponseWriter, *Request) condResult { := .Header.Get("If-Match")if == "" {returncondNone }for { = textproto.TrimString()iflen() == 0 {break }if [0] == ',' { = [1:]continue }if [0] == '*' {returncondTrue } , := scanETag()if == "" {break }ifetagStrongMatch(, .Header().get("Etag")) {returncondTrue } = }returncondFalse}func ( *Request, time.Time) condResult { := .Header.Get("If-Unmodified-Since")if == "" || isZeroTime() {returncondNone } , := ParseTime()if != nil {returncondNone }// The Last-Modified header truncates sub-second precision so // the modtime needs to be truncated too. = .Truncate(time.Second)if := .Compare(); <= 0 {returncondTrue }returncondFalse}func ( ResponseWriter, *Request) condResult { := .Header.get("If-None-Match")if == "" {returncondNone } := for { = textproto.TrimString()iflen() == 0 {break }if [0] == ',' { = [1:]continue }if [0] == '*' {returncondFalse } , := scanETag()if == "" {break }ifetagWeakMatch(, .Header().get("Etag")) {returncondFalse } = }returncondTrue}func ( *Request, time.Time) condResult {if .Method != "GET" && .Method != "HEAD" {returncondNone } := .Header.Get("If-Modified-Since")if == "" || isZeroTime() {returncondNone } , := ParseTime()if != nil {returncondNone }// The Last-Modified header truncates sub-second precision so // the modtime needs to be truncated too. = .Truncate(time.Second)if := .Compare(); <= 0 {returncondFalse }returncondTrue}func ( ResponseWriter, *Request, time.Time) condResult {if .Method != "GET" && .Method != "HEAD" {returncondNone } := .Header.get("If-Range")if == "" {returncondNone } , := scanETag()if != "" {ifetagStrongMatch(, .Header().Get("Etag")) {returncondTrue } else {returncondFalse } }// The If-Range value is typically the ETag value, but it may also be // the modtime date. See golang.org/issue/8367.if .IsZero() {returncondFalse } , := ParseTime()if != nil {returncondFalse }if .Unix() == .Unix() {returncondTrue }returncondFalse}varunixEpochTime = time.Unix(0, 0)// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).func ( time.Time) bool {return .IsZero() || .Equal(unixEpochTime)}func ( ResponseWriter, time.Time) {if !isZeroTime() { .Header().Set("Last-Modified", .UTC().Format(TimeFormat)) }}func ( ResponseWriter) {// RFC 7232 section 4.1: // a sender SHOULD NOT generate representation metadata other than the // above listed fields unless said metadata exists for the purpose of // guiding cache updates (e.g., Last-Modified might be useful if the // response does not have an ETag field). := .Header()delete(, "Content-Type")delete(, "Content-Length")delete(, "Content-Encoding")if .Get("Etag") != "" {delete(, "Last-Modified") } .WriteHeader(StatusNotModified)}// checkPreconditions evaluates request preconditions and reports whether a precondition// resulted in sending StatusNotModified or StatusPreconditionFailed.func ( ResponseWriter, *Request, time.Time) ( bool, string) {// This function carefully follows RFC 7232 section 6. := checkIfMatch(, )if == condNone { = checkIfUnmodifiedSince(, ) }if == condFalse { .WriteHeader(StatusPreconditionFailed)returntrue, "" }switchcheckIfNoneMatch(, ) {casecondFalse:if .Method == "GET" || .Method == "HEAD" {writeNotModified()returntrue, "" } else { .WriteHeader(StatusPreconditionFailed)returntrue, "" }casecondNone:ifcheckIfModifiedSince(, ) == condFalse {writeNotModified()returntrue, "" } } = .Header.get("Range")if != "" && checkIfRange(, , ) == condFalse { = "" }returnfalse, }// name is '/'-separated, not filepath.Separator.func ( ResponseWriter, *Request, FileSystem, string, bool) {const = "/index.html"// redirect .../index.html to .../ // can't use Redirect() because that would make the path absolute, // which would be a problem running under StripPrefixifstrings.HasSuffix(.URL.Path, ) {localRedirect(, , "./")return } , := .Open()if != nil { , := toHTTPError()Error(, , )return }defer .Close() , := .Stat()if != nil { , := toHTTPError()Error(, , )return }if {// redirect to canonical path: / at end of directory url // r.URL.Path always begins with / := .URL.Pathif .IsDir() {if [len()-1] != '/' {localRedirect(, , path.Base()+"/")return } } else {if [len()-1] == '/' {localRedirect(, , "../"+path.Base())return } } }if .IsDir() { := .URL.Path// redirect if the directory name doesn't end in a slashif == "" || [len()-1] != '/' {localRedirect(, , path.Base()+"/")return }// use contents of index.html for directory, if present := strings.TrimSuffix(, "/") + , := .Open()if == nil {defer .Close() , := .Stat()if == nil { = = } } }// Still a directory? (we didn't find an index.html file)if .IsDir() {ifcheckIfModifiedSince(, .ModTime()) == condFalse {writeNotModified()return }setLastModified(, .ModTime())dirList(, , )return }// serveContent will check modification time := func() (int64, error) { return .Size(), nil }serveContent(, , .Name(), .ModTime(), , )}// toHTTPError returns a non-specific HTTP error message and status code// for a given non-nil error value. It's important that toHTTPError does not// actually return err.Error(), since msg and httpStatus are returned to users,// and historically Go's ServeContent always returned just "404 Not Found" for// all errors. We don't want to start leaking information in error messages.func ( error) ( string, int) {iferrors.Is(, fs.ErrNotExist) {return"404 page not found", StatusNotFound }iferrors.Is(, fs.ErrPermission) {return"403 Forbidden", StatusForbidden }// Default:return"500 Internal Server Error", StatusInternalServerError}// localRedirect gives a Moved Permanently response.// It does not convert relative paths to absolute paths like Redirect does.func ( ResponseWriter, *Request, string) {if := .URL.RawQuery; != "" { += "?" + } .Header().Set("Location", ) .WriteHeader(StatusMovedPermanently)}// ServeFile replies to the request with the contents of the named// file or directory.//// If the provided file or directory name is a relative path, it is// interpreted relative to the current directory and may ascend to// parent directories. If the provided name is constructed from user// input, it should be sanitized before calling ServeFile.//// As a precaution, ServeFile will reject requests where r.URL.Path// contains a ".." path element; this protects against callers who// might unsafely use filepath.Join on r.URL.Path without sanitizing// it and then use that filepath.Join result as the name argument.//// As another special case, ServeFile redirects any request where r.URL.Path// ends in "/index.html" to the same path, without the final// "index.html". To avoid such redirects either modify the path or// use ServeContent.//// Outside of those two special cases, ServeFile does not use// r.URL.Path for selecting the file or directory to serve; only the// file or directory provided in the name argument is used.func ( ResponseWriter, *Request, string) {ifcontainsDotDot(.URL.Path) {// Too many programs use r.URL.Path to construct the argument to // serveFile. Reject the request under the assumption that happened // here and ".." may not be wanted. // Note that name might not contain "..", for example if code (still // incorrectly) used filepath.Join(myDir, r.URL.Path).Error(, "invalid URL path", StatusBadRequest)return } , := filepath.Split()serveFile(, , Dir(), , false)}func ( string) bool {if !strings.Contains(, "..") {returnfalse }for , := rangestrings.FieldsFunc(, isSlashRune) {if == ".." {returntrue } }returnfalse}func ( rune) bool { return == '/' || == '\\' }typefileHandlerstruct {rootFileSystem}typeioFSstruct {fsysfs.FS}typeioFilestruct {filefs.File}func ( ioFS) ( string) (File, error) {if == "/" { = "." } else { = strings.TrimPrefix(, "/") } , := .fsys.Open()if != nil {returnnil, mapOpenError(, , '/', func( string) (fs.FileInfo, error) {returnfs.Stat(.fsys, ) }) }returnioFile{}, nil}func ( ioFile) () error { return .file.Close() }func ( ioFile) ( []byte) (int, error) { return .file.Read() }func ( ioFile) () (fs.FileInfo, error) { return .file.Stat() }varerrMissingSeek = errors.New("io.File missing Seek method")varerrMissingReadDir = errors.New("io.File directory missing ReadDir method")func ( ioFile) ( int64, int) (int64, error) { , := .file.(io.Seeker)if ! {return0, errMissingSeek }return .Seek(, )}func ( ioFile) ( int) ([]fs.DirEntry, error) { , := .file.(fs.ReadDirFile)if ! {returnnil, errMissingReadDir }return .ReadDir()}func ( ioFile) ( int) ([]fs.FileInfo, error) { , := .file.(fs.ReadDirFile)if ! {returnnil, errMissingReadDir }var []fs.FileInfofor { , := .ReadDir( - len())for , := range { , := .Info()if != nil {// Pretend it doesn't exist, like (*os.File).Readdir does.continue } = append(, ) }if != nil {return , }if < 0 || len() >= {break } }return , nil}// FS converts fsys to a FileSystem implementation,// for use with FileServer and NewFileTransport.// The files provided by fsys must implement io.Seeker.func ( fs.FS) FileSystem {returnioFS{}}// FileServer returns a handler that serves HTTP requests// with the contents of the file system rooted at root.//// As a special case, the returned file server redirects any request// ending in "/index.html" to the same path, without the final// "index.html".//// To use the operating system's file system implementation,// use http.Dir://// http.Handle("/", http.FileServer(http.Dir("/tmp")))//// To use an fs.FS implementation, use http.FS to convert it://// http.Handle("/", http.FileServer(http.FS(fsys)))func ( FileSystem) Handler {return &fileHandler{}}func ( *fileHandler) ( ResponseWriter, *Request) { := .URL.Pathif !strings.HasPrefix(, "/") { = "/" + .URL.Path = }serveFile(, , .root, path.Clean(), true)}// httpRange specifies the byte range to be sent to the client.typehttpRangestruct {start, lengthint64}func ( httpRange) ( int64) string {returnfmt.Sprintf("bytes %d-%d/%d", .start, .start+.length-1, )}func ( httpRange) ( string, int64) textproto.MIMEHeader {returntextproto.MIMEHeader{"Content-Range": {.contentRange()},"Content-Type": {}, }}// parseRange parses a Range header string as per RFC 7233.// errNoOverlap is returned if none of the ranges overlap.func ( string, int64) ([]httpRange, error) {if == "" {returnnil, nil// header not present }const = "bytes="if !strings.HasPrefix(, ) {returnnil, errors.New("invalid range") }var []httpRange := falsefor , := rangestrings.Split([len():], ",") { = textproto.TrimString()if == "" {continue } , , := strings.Cut(, "-")if ! {returnnil, errors.New("invalid range") } , = textproto.TrimString(), textproto.TrimString()varhttpRangeif == "" {// If no start is specified, end specifies the // range start relative to the end of the file, // and we are dealing with <suffix-length> // which has to be a non-negative integer as per // RFC 7233 Section 2.1 "Byte-Ranges".if == "" || [0] == '-' {returnnil, errors.New("invalid range") } , := strconv.ParseInt(, 10, 64)if < 0 || != nil {returnnil, errors.New("invalid range") }if > { = } .start = - .length = - .start } else { , := strconv.ParseInt(, 10, 64)if != nil || < 0 {returnnil, errors.New("invalid range") }if >= {// If the range begins after the size of the content, // then it does not overlap. = truecontinue } .start = if == "" {// If no end is specified, range extends to end of the file. .length = - .start } else { , := strconv.ParseInt(, 10, 64)if != nil || .start > {returnnil, errors.New("invalid range") }if >= { = - 1 } .length = - .start + 1 } } = append(, ) }if && len() == 0 {// The specified ranges did not overlap with the content.returnnil, errNoOverlap }return , nil}// countingWriter counts how many bytes have been written to it.typecountingWriterint64func ( *countingWriter) ( []byte) ( int, error) { * += countingWriter(len())returnlen(), nil}// rangesMIMESize returns the number of bytes it takes to encode the// provided ranges as a multipart response.func ( []httpRange, string, int64) ( int64) {varcountingWriter := multipart.NewWriter(&)for , := range { .CreatePart(.mimeHeader(, )) += .length } .Close() += int64()return}func ( []httpRange) ( int64) {for , := range { += .length }return}
The pages are generated with Goldsv0.6.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds.