github.com/riscv/riscv-go@v0.0.0-20200123204226-124ebd6fcc8e/src/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 "io" 13 "mime" 14 "mime/multipart" 15 "net/textproto" 16 "net/url" 17 "os" 18 "path" 19 "path/filepath" 20 "sort" 21 "strconv" 22 "strings" 23 "time" 24 ) 25 26 // A Dir implements FileSystem using the native file system restricted to a 27 // specific directory tree. 28 // 29 // While the FileSystem.Open method takes '/'-separated paths, a Dir's string 30 // value is a filename on the native file system, not a URL, so it is separated 31 // by filepath.Separator, which isn't necessarily '/'. 32 // 33 // An empty Dir is treated as ".". 34 type Dir string 35 36 func (d Dir) Open(name string) (File, error) { 37 if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { 38 return nil, errors.New("http: invalid character in file path") 39 } 40 dir := string(d) 41 if dir == "" { 42 dir = "." 43 } 44 f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))) 45 if err != nil { 46 return nil, err 47 } 48 return f, nil 49 } 50 51 // A FileSystem implements access to a collection of named files. 52 // The elements in a file path are separated by slash ('/', U+002F) 53 // characters, regardless of host operating system convention. 54 type FileSystem interface { 55 Open(name string) (File, error) 56 } 57 58 // A File is returned by a FileSystem's Open method and can be 59 // served by the FileServer implementation. 60 // 61 // The methods should behave the same as those on an *os.File. 62 type File interface { 63 io.Closer 64 io.Reader 65 io.Seeker 66 Readdir(count int) ([]os.FileInfo, error) 67 Stat() (os.FileInfo, error) 68 } 69 70 func dirList(w ResponseWriter, f File) { 71 dirs, err := f.Readdir(-1) 72 if err != nil { 73 // TODO: log err.Error() to the Server.ErrorLog, once it's possible 74 // for a handler to get at its Server via the ResponseWriter. See 75 // Issue 12438. 76 Error(w, "Error reading directory", StatusInternalServerError) 77 return 78 } 79 sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) 80 81 w.Header().Set("Content-Type", "text/html; charset=utf-8") 82 fmt.Fprintf(w, "<pre>\n") 83 for _, d := range dirs { 84 name := d.Name() 85 if d.IsDir() { 86 name += "/" 87 } 88 // name may contain '?' or '#', which must be escaped to remain 89 // part of the URL path, and not indicate the start of a query 90 // string or fragment. 91 url := url.URL{Path: name} 92 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name)) 93 } 94 fmt.Fprintf(w, "</pre>\n") 95 } 96 97 // ServeContent replies to the request using the content in the 98 // provided ReadSeeker. The main benefit of ServeContent over io.Copy 99 // is that it handles Range requests properly, sets the MIME type, and 100 // handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since, 101 // and If-Range requests. 102 // 103 // If the response's Content-Type header is not set, ServeContent 104 // first tries to deduce the type from name's file extension and, 105 // if that fails, falls back to reading the first block of the content 106 // and passing it to DetectContentType. 107 // The name is otherwise unused; in particular it can be empty and is 108 // never sent in the response. 109 // 110 // If modtime is not the zero time or Unix epoch, ServeContent 111 // includes it in a Last-Modified header in the response. If the 112 // request includes an If-Modified-Since header, ServeContent uses 113 // modtime to decide whether the content needs to be sent at all. 114 // 115 // The content's Seek method must work: ServeContent uses 116 // a seek to the end of the content to determine its size. 117 // 118 // If the caller has set w's ETag header formatted per RFC 7232, section 2.3, 119 // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range. 120 // 121 // Note that *os.File implements the io.ReadSeeker interface. 122 func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) { 123 sizeFunc := func() (int64, error) { 124 size, err := content.Seek(0, io.SeekEnd) 125 if err != nil { 126 return 0, errSeeker 127 } 128 _, err = content.Seek(0, io.SeekStart) 129 if err != nil { 130 return 0, errSeeker 131 } 132 return size, nil 133 } 134 serveContent(w, req, name, modtime, sizeFunc, content) 135 } 136 137 // errSeeker is returned by ServeContent's sizeFunc when the content 138 // doesn't seek properly. The underlying Seeker's error text isn't 139 // included in the sizeFunc reply so it's not sent over HTTP to end 140 // users. 141 var errSeeker = errors.New("seeker can't seek") 142 143 // errNoOverlap is returned by serveContent's parseRange if first-byte-pos of 144 // all of the byte-range-spec values is greater than the content size. 145 var errNoOverlap = errors.New("invalid range: failed to overlap") 146 147 // if name is empty, filename is unknown. (used for mime type, before sniffing) 148 // if modtime.IsZero(), modtime is unknown. 149 // content must be seeked to the beginning of the file. 150 // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. 151 func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) { 152 setLastModified(w, modtime) 153 done, rangeReq := checkPreconditions(w, r, modtime) 154 if done { 155 return 156 } 157 158 code := StatusOK 159 160 // If Content-Type isn't set, use the file's extension to find it, but 161 // if the Content-Type is unset explicitly, do not sniff the type. 162 ctypes, haveType := w.Header()["Content-Type"] 163 var ctype string 164 if !haveType { 165 ctype = mime.TypeByExtension(filepath.Ext(name)) 166 if ctype == "" { 167 // read a chunk to decide between utf-8 text and binary 168 var buf [sniffLen]byte 169 n, _ := io.ReadFull(content, buf[:]) 170 ctype = DetectContentType(buf[:n]) 171 _, err := content.Seek(0, io.SeekStart) // rewind to output whole file 172 if err != nil { 173 Error(w, "seeker can't seek", StatusInternalServerError) 174 return 175 } 176 } 177 w.Header().Set("Content-Type", ctype) 178 } else if len(ctypes) > 0 { 179 ctype = ctypes[0] 180 } 181 182 size, err := sizeFunc() 183 if err != nil { 184 Error(w, err.Error(), StatusInternalServerError) 185 return 186 } 187 188 // handle Content-Range header. 189 sendSize := size 190 var sendContent io.Reader = content 191 if size >= 0 { 192 ranges, err := parseRange(rangeReq, size) 193 if err != nil { 194 if err == errNoOverlap { 195 w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size)) 196 } 197 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) 198 return 199 } 200 if sumRangesSize(ranges) > size { 201 // The total number of bytes in all the ranges 202 // is larger than the size of the file by 203 // itself, so this is probably an attack, or a 204 // dumb client. Ignore the range request. 205 ranges = nil 206 } 207 switch { 208 case len(ranges) == 1: 209 // RFC 2616, Section 14.16: 210 // "When an HTTP message includes the content of a single 211 // range (for example, a response to a request for a 212 // single range, or to a request for a set of ranges 213 // that overlap without any holes), this content is 214 // transmitted with a Content-Range header, and a 215 // Content-Length header showing the number of bytes 216 // actually transferred. 217 // ... 218 // A response to a request for a single range MUST NOT 219 // be sent using the multipart/byteranges media type." 220 ra := ranges[0] 221 if _, err := content.Seek(ra.start, io.SeekStart); err != nil { 222 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) 223 return 224 } 225 sendSize = ra.length 226 code = StatusPartialContent 227 w.Header().Set("Content-Range", ra.contentRange(size)) 228 case len(ranges) > 1: 229 sendSize = rangesMIMESize(ranges, ctype, size) 230 code = StatusPartialContent 231 232 pr, pw := io.Pipe() 233 mw := multipart.NewWriter(pw) 234 w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary()) 235 sendContent = pr 236 defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish. 237 go func() { 238 for _, ra := range ranges { 239 part, err := mw.CreatePart(ra.mimeHeader(ctype, size)) 240 if err != nil { 241 pw.CloseWithError(err) 242 return 243 } 244 if _, err := content.Seek(ra.start, io.SeekStart); err != nil { 245 pw.CloseWithError(err) 246 return 247 } 248 if _, err := io.CopyN(part, content, ra.length); err != nil { 249 pw.CloseWithError(err) 250 return 251 } 252 } 253 mw.Close() 254 pw.Close() 255 }() 256 } 257 258 w.Header().Set("Accept-Ranges", "bytes") 259 if w.Header().Get("Content-Encoding") == "" { 260 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10)) 261 } 262 } 263 264 w.WriteHeader(code) 265 266 if r.Method != "HEAD" { 267 io.CopyN(w, sendContent, sendSize) 268 } 269 } 270 271 // scanETag determines if a syntactically valid ETag is present at s. If so, 272 // the ETag and remaining text after consuming ETag is returned. Otherwise, 273 // it returns "", "". 274 func scanETag(s string) (etag string, remain string) { 275 s = textproto.TrimString(s) 276 start := 0 277 if strings.HasPrefix(s, "W/") { 278 start = 2 279 } 280 if len(s[start:]) < 2 || s[start] != '"' { 281 return "", "" 282 } 283 // ETag is either W/"text" or "text". 284 // See RFC 7232 2.3. 285 for i := start + 1; i < len(s); i++ { 286 c := s[i] 287 switch { 288 // Character values allowed in ETags. 289 case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80: 290 case c == '"': 291 return string(s[:i+1]), s[i+1:] 292 default: 293 break 294 } 295 } 296 return "", "" 297 } 298 299 // etagStrongMatch reports whether a and b match using strong ETag comparison. 300 // Assumes a and b are valid ETags. 301 func etagStrongMatch(a, b string) bool { 302 return a == b && a != "" && a[0] == '"' 303 } 304 305 // etagWeakMatch reports whether a and b match using weak ETag comparison. 306 // Assumes a and b are valid ETags. 307 func etagWeakMatch(a, b string) bool { 308 return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/") 309 } 310 311 // condResult is the result of an HTTP request precondition check. 312 // See https://tools.ietf.org/html/rfc7232 section 3. 313 type condResult int 314 315 const ( 316 condNone condResult = iota 317 condTrue 318 condFalse 319 ) 320 321 func checkIfMatch(w ResponseWriter, r *Request) condResult { 322 im := r.Header.Get("If-Match") 323 if im == "" { 324 return condNone 325 } 326 for { 327 im = textproto.TrimString(im) 328 if len(im) == 0 { 329 break 330 } 331 if im[0] == ',' { 332 im = im[1:] 333 continue 334 } 335 if im[0] == '*' { 336 return condTrue 337 } 338 etag, remain := scanETag(im) 339 if etag == "" { 340 break 341 } 342 if etagStrongMatch(etag, w.Header().get("Etag")) { 343 return condTrue 344 } 345 im = remain 346 } 347 348 return condFalse 349 } 350 351 func checkIfUnmodifiedSince(w ResponseWriter, r *Request, modtime time.Time) condResult { 352 ius := r.Header.Get("If-Unmodified-Since") 353 if ius == "" || isZeroTime(modtime) { 354 return condNone 355 } 356 if t, err := ParseTime(ius); err == nil { 357 // The Date-Modified header truncates sub-second precision, so 358 // use mtime < t+1s instead of mtime <= t to check for unmodified. 359 if modtime.Before(t.Add(1 * time.Second)) { 360 return condTrue 361 } 362 return condFalse 363 } 364 return condNone 365 } 366 367 func checkIfNoneMatch(w ResponseWriter, r *Request) condResult { 368 inm := r.Header.get("If-None-Match") 369 if inm == "" { 370 return condNone 371 } 372 buf := inm 373 for { 374 buf = textproto.TrimString(buf) 375 if len(buf) == 0 { 376 break 377 } 378 if buf[0] == ',' { 379 buf = buf[1:] 380 } 381 if buf[0] == '*' { 382 return condFalse 383 } 384 etag, remain := scanETag(buf) 385 if etag == "" { 386 break 387 } 388 if etagWeakMatch(etag, w.Header().get("Etag")) { 389 return condFalse 390 } 391 buf = remain 392 } 393 return condTrue 394 } 395 396 func checkIfModifiedSince(w ResponseWriter, r *Request, modtime time.Time) condResult { 397 if r.Method != "GET" && r.Method != "HEAD" { 398 return condNone 399 } 400 ims := r.Header.Get("If-Modified-Since") 401 if ims == "" || isZeroTime(modtime) { 402 return condNone 403 } 404 t, err := ParseTime(ims) 405 if err != nil { 406 return condNone 407 } 408 // The Date-Modified header truncates sub-second precision, so 409 // use mtime < t+1s instead of mtime <= t to check for unmodified. 410 if modtime.Before(t.Add(1 * time.Second)) { 411 return condFalse 412 } 413 return condTrue 414 } 415 416 func checkIfRange(w ResponseWriter, r *Request, modtime time.Time) condResult { 417 if r.Method != "GET" { 418 return condNone 419 } 420 ir := r.Header.get("If-Range") 421 if ir == "" { 422 return condNone 423 } 424 etag, _ := scanETag(ir) 425 if etag != "" { 426 if etagStrongMatch(etag, w.Header().Get("Etag")) { 427 return condTrue 428 } else { 429 return condFalse 430 } 431 } 432 // The If-Range value is typically the ETag value, but it may also be 433 // the modtime date. See golang.org/issue/8367. 434 if modtime.IsZero() { 435 return condFalse 436 } 437 t, err := ParseTime(ir) 438 if err != nil { 439 return condFalse 440 } 441 if t.Unix() == modtime.Unix() { 442 return condTrue 443 } 444 return condFalse 445 } 446 447 var unixEpochTime = time.Unix(0, 0) 448 449 // isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). 450 func isZeroTime(t time.Time) bool { 451 return t.IsZero() || t.Equal(unixEpochTime) 452 } 453 454 func setLastModified(w ResponseWriter, modtime time.Time) { 455 if !isZeroTime(modtime) { 456 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat)) 457 } 458 } 459 460 func writeNotModified(w ResponseWriter) { 461 // RFC 7232 section 4.1: 462 // a sender SHOULD NOT generate representation metadata other than the 463 // above listed fields unless said metadata exists for the purpose of 464 // guiding cache updates (e.g., Last-Modified might be useful if the 465 // response does not have an ETag field). 466 h := w.Header() 467 delete(h, "Content-Type") 468 delete(h, "Content-Length") 469 if h.Get("Etag") != "" { 470 delete(h, "Last-Modified") 471 } 472 w.WriteHeader(StatusNotModified) 473 } 474 475 // checkPreconditions evaluates request preconditions and reports whether a precondition 476 // resulted in sending StatusNotModified or StatusPreconditionFailed. 477 func checkPreconditions(w ResponseWriter, r *Request, modtime time.Time) (done bool, rangeHeader string) { 478 // This function carefully follows RFC 7232 section 6. 479 ch := checkIfMatch(w, r) 480 if ch == condNone { 481 ch = checkIfUnmodifiedSince(w, r, modtime) 482 } 483 if ch == condFalse { 484 w.WriteHeader(StatusPreconditionFailed) 485 return true, "" 486 } 487 switch checkIfNoneMatch(w, r) { 488 case condFalse: 489 if r.Method == "GET" || r.Method == "HEAD" { 490 writeNotModified(w) 491 return true, "" 492 } else { 493 w.WriteHeader(StatusPreconditionFailed) 494 return true, "" 495 } 496 case condNone: 497 if checkIfModifiedSince(w, r, modtime) == condFalse { 498 writeNotModified(w) 499 return true, "" 500 } 501 } 502 503 rangeHeader = r.Header.get("Range") 504 if rangeHeader != "" { 505 if checkIfRange(w, r, modtime) == condFalse { 506 rangeHeader = "" 507 } 508 } 509 return false, rangeHeader 510 } 511 512 // name is '/'-separated, not filepath.Separator. 513 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) { 514 const indexPage = "/index.html" 515 516 // redirect .../index.html to .../ 517 // can't use Redirect() because that would make the path absolute, 518 // which would be a problem running under StripPrefix 519 if strings.HasSuffix(r.URL.Path, indexPage) { 520 localRedirect(w, r, "./") 521 return 522 } 523 524 f, err := fs.Open(name) 525 if err != nil { 526 msg, code := toHTTPError(err) 527 Error(w, msg, code) 528 return 529 } 530 defer f.Close() 531 532 d, err := f.Stat() 533 if err != nil { 534 msg, code := toHTTPError(err) 535 Error(w, msg, code) 536 return 537 } 538 539 if redirect { 540 // redirect to canonical path: / at end of directory url 541 // r.URL.Path always begins with / 542 url := r.URL.Path 543 if d.IsDir() { 544 if url[len(url)-1] != '/' { 545 localRedirect(w, r, path.Base(url)+"/") 546 return 547 } 548 } else { 549 if url[len(url)-1] == '/' { 550 localRedirect(w, r, "../"+path.Base(url)) 551 return 552 } 553 } 554 } 555 556 // redirect if the directory name doesn't end in a slash 557 if d.IsDir() { 558 url := r.URL.Path 559 if url[len(url)-1] != '/' { 560 localRedirect(w, r, path.Base(url)+"/") 561 return 562 } 563 } 564 565 // use contents of index.html for directory, if present 566 if d.IsDir() { 567 index := strings.TrimSuffix(name, "/") + indexPage 568 ff, err := fs.Open(index) 569 if err == nil { 570 defer ff.Close() 571 dd, err := ff.Stat() 572 if err == nil { 573 name = index 574 d = dd 575 f = ff 576 } 577 } 578 } 579 580 // Still a directory? (we didn't find an index.html file) 581 if d.IsDir() { 582 if checkIfModifiedSince(w, r, d.ModTime()) == condFalse { 583 writeNotModified(w) 584 return 585 } 586 w.Header().Set("Last-Modified", d.ModTime().UTC().Format(TimeFormat)) 587 dirList(w, f) 588 return 589 } 590 591 // serveContent will check modification time 592 sizeFunc := func() (int64, error) { return d.Size(), nil } 593 serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f) 594 } 595 596 // toHTTPError returns a non-specific HTTP error message and status code 597 // for a given non-nil error value. It's important that toHTTPError does not 598 // actually return err.Error(), since msg and httpStatus are returned to users, 599 // and historically Go's ServeContent always returned just "404 Not Found" for 600 // all errors. We don't want to start leaking information in error messages. 601 func toHTTPError(err error) (msg string, httpStatus int) { 602 if os.IsNotExist(err) { 603 return "404 page not found", StatusNotFound 604 } 605 if os.IsPermission(err) { 606 return "403 Forbidden", StatusForbidden 607 } 608 // Default: 609 return "500 Internal Server Error", StatusInternalServerError 610 } 611 612 // localRedirect gives a Moved Permanently response. 613 // It does not convert relative paths to absolute paths like Redirect does. 614 func localRedirect(w ResponseWriter, r *Request, newPath string) { 615 if q := r.URL.RawQuery; q != "" { 616 newPath += "?" + q 617 } 618 w.Header().Set("Location", newPath) 619 w.WriteHeader(StatusMovedPermanently) 620 } 621 622 // ServeFile replies to the request with the contents of the named 623 // file or directory. 624 // 625 // If the provided file or directory name is a relative path, it is 626 // interpreted relative to the current directory and may ascend to parent 627 // directories. If the provided name is constructed from user input, it 628 // should be sanitized before calling ServeFile. As a precaution, ServeFile 629 // will reject requests where r.URL.Path contains a ".." path element. 630 // 631 // As a special case, ServeFile redirects any request where r.URL.Path 632 // ends in "/index.html" to the same path, without the final 633 // "index.html". To avoid such redirects either modify the path or 634 // use ServeContent. 635 func ServeFile(w ResponseWriter, r *Request, name string) { 636 if containsDotDot(r.URL.Path) { 637 // Too many programs use r.URL.Path to construct the argument to 638 // serveFile. Reject the request under the assumption that happened 639 // here and ".." may not be wanted. 640 // Note that name might not contain "..", for example if code (still 641 // incorrectly) used filepath.Join(myDir, r.URL.Path). 642 Error(w, "invalid URL path", StatusBadRequest) 643 return 644 } 645 dir, file := filepath.Split(name) 646 serveFile(w, r, Dir(dir), file, false) 647 } 648 649 func containsDotDot(v string) bool { 650 if !strings.Contains(v, "..") { 651 return false 652 } 653 for _, ent := range strings.FieldsFunc(v, isSlashRune) { 654 if ent == ".." { 655 return true 656 } 657 } 658 return false 659 } 660 661 func isSlashRune(r rune) bool { return r == '/' || r == '\\' } 662 663 type fileHandler struct { 664 root FileSystem 665 } 666 667 // FileServer returns a handler that serves HTTP requests 668 // with the contents of the file system rooted at root. 669 // 670 // To use the operating system's file system implementation, 671 // use http.Dir: 672 // 673 // http.Handle("/", http.FileServer(http.Dir("/tmp"))) 674 // 675 // As a special case, the returned file server redirects any request 676 // ending in "/index.html" to the same path, without the final 677 // "index.html". 678 func FileServer(root FileSystem) Handler { 679 return &fileHandler{root} 680 } 681 682 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { 683 upath := r.URL.Path 684 if !strings.HasPrefix(upath, "/") { 685 upath = "/" + upath 686 r.URL.Path = upath 687 } 688 serveFile(w, r, f.root, path.Clean(upath), true) 689 } 690 691 // httpRange specifies the byte range to be sent to the client. 692 type httpRange struct { 693 start, length int64 694 } 695 696 func (r httpRange) contentRange(size int64) string { 697 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size) 698 } 699 700 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader { 701 return textproto.MIMEHeader{ 702 "Content-Range": {r.contentRange(size)}, 703 "Content-Type": {contentType}, 704 } 705 } 706 707 // parseRange parses a Range header string as per RFC 2616. 708 // errNoOverlap is returned if none of the ranges overlap. 709 func parseRange(s string, size int64) ([]httpRange, error) { 710 if s == "" { 711 return nil, nil // header not present 712 } 713 const b = "bytes=" 714 if !strings.HasPrefix(s, b) { 715 return nil, errors.New("invalid range") 716 } 717 var ranges []httpRange 718 noOverlap := false 719 for _, ra := range strings.Split(s[len(b):], ",") { 720 ra = strings.TrimSpace(ra) 721 if ra == "" { 722 continue 723 } 724 i := strings.Index(ra, "-") 725 if i < 0 { 726 return nil, errors.New("invalid range") 727 } 728 start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:]) 729 var r httpRange 730 if start == "" { 731 // If no start is specified, end specifies the 732 // range start relative to the end of the file. 733 i, err := strconv.ParseInt(end, 10, 64) 734 if err != nil { 735 return nil, errors.New("invalid range") 736 } 737 if i > size { 738 i = size 739 } 740 r.start = size - i 741 r.length = size - r.start 742 } else { 743 i, err := strconv.ParseInt(start, 10, 64) 744 if err != nil || i < 0 { 745 return nil, errors.New("invalid range") 746 } 747 if i >= size { 748 // If the range begins after the size of the content, 749 // then it does not overlap. 750 noOverlap = true 751 continue 752 } 753 r.start = i 754 if end == "" { 755 // If no end is specified, range extends to end of the file. 756 r.length = size - r.start 757 } else { 758 i, err := strconv.ParseInt(end, 10, 64) 759 if err != nil || r.start > i { 760 return nil, errors.New("invalid range") 761 } 762 if i >= size { 763 i = size - 1 764 } 765 r.length = i - r.start + 1 766 } 767 } 768 ranges = append(ranges, r) 769 } 770 if noOverlap && len(ranges) == 0 { 771 // The specified ranges did not overlap with the content. 772 return nil, errNoOverlap 773 } 774 return ranges, nil 775 } 776 777 // countingWriter counts how many bytes have been written to it. 778 type countingWriter int64 779 780 func (w *countingWriter) Write(p []byte) (n int, err error) { 781 *w += countingWriter(len(p)) 782 return len(p), nil 783 } 784 785 // rangesMIMESize returns the number of bytes it takes to encode the 786 // provided ranges as a multipart response. 787 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) { 788 var w countingWriter 789 mw := multipart.NewWriter(&w) 790 for _, ra := range ranges { 791 mw.CreatePart(ra.mimeHeader(contentType, contentSize)) 792 encSize += ra.length 793 } 794 mw.Close() 795 encSize += int64(w) 796 return 797 } 798 799 func sumRangesSize(ranges []httpRange) (size int64) { 800 for _, ra := range ranges { 801 size += ra.length 802 } 803 return 804 }