...
Run Format

Source file 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	}
   806	

View as plain text