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