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