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