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.
5 // HTTP file system request handler
25 // A Dir implements FileSystem using the native file system restricted to a
26 // specific directory tree.
28 // While the FileSystem.Open method takes '/'-separated paths, a Dir's string
29 // value is a filename on the native file system, not a URL, so it is separated
30 // by filepath.Separator, which isn't necessarily '/'.
32 // An empty Dir is treated as ".".
35 func (d Dir
) Open(name
string) (File
, error
) {
36 if filepath
.Separator
!= '/' && strings
.IndexRune(name
, filepath
.Separator
) >= 0 ||
37 strings
.Contains(name
, "\x00") {
38 return nil, errors
.New("http: invalid character in file path")
44 f
, err
:= os
.Open(filepath
.Join(dir
, filepath
.FromSlash(path
.Clean("/"+name
))))
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
)
58 // A File is returned by a FileSystem's Open method and can be
59 // served by the FileServer implementation.
61 // The methods should behave the same as those on an *os.File.
65 Readdir(count
int) ([]os
.FileInfo
, error
)
66 Seek(offset
int64, whence
int) (int64, error
)
67 Stat() (os
.FileInfo
, error
)
70 func dirList(w ResponseWriter
, f File
) {
71 w
.Header().Set("Content-Type", "text/html; charset=utf-8")
72 fmt
.Fprintf(w
, "<pre>\n")
74 dirs
, err
:= f
.Readdir(100)
75 if err
!= nil ||
len(dirs
) == 0 {
78 for _
, d
:= range dirs
{
83 // name may contain '?' or '#', which must be escaped to remain
84 // part of the URL path, and not indicate the start of a query
85 // string or fragment.
86 url
:= url
.URL
{Path
: name
}
87 fmt
.Fprintf(w
, "<a href=\"%s\">%s</a>\n", url
.String(), htmlReplacer
.Replace(name
))
90 fmt
.Fprintf(w
, "</pre>\n")
93 // ServeContent replies to the request using the content in the
94 // provided ReadSeeker. The main benefit of ServeContent over io.Copy
95 // is that it handles Range requests properly, sets the MIME type, and
96 // handles If-Modified-Since requests.
98 // If the response's Content-Type header is not set, ServeContent
99 // first tries to deduce the type from name's file extension and,
100 // if that fails, falls back to reading the first block of the content
101 // and passing it to DetectContentType.
102 // The name is otherwise unused; in particular it can be empty and is
103 // never sent in the response.
105 // If modtime is not the zero time, ServeContent includes it in a
106 // Last-Modified header in the response. If the request includes an
107 // If-Modified-Since header, ServeContent uses modtime to decide
108 // whether the content needs to be sent at all.
110 // The content's Seek method must work: ServeContent uses
111 // a seek to the end of the content to determine its size.
113 // If the caller has set w's ETag header, ServeContent uses it to
114 // handle requests using If-Range and If-None-Match.
116 // Note that *os.File implements the io.ReadSeeker interface.
117 func ServeContent(w ResponseWriter
, req
*Request
, name
string, modtime time
.Time
, content io
.ReadSeeker
) {
118 sizeFunc
:= func() (int64, error
) {
119 size
, err
:= content
.Seek(0, os
.SEEK_END
)
123 _
, err
= content
.Seek(0, os
.SEEK_SET
)
129 serveContent(w
, req
, name
, modtime
, sizeFunc
, content
)
132 // errSeeker is returned by ServeContent's sizeFunc when the content
133 // doesn't seek properly. The underlying Seeker's error text isn't
134 // included in the sizeFunc reply so it's not sent over HTTP to end
136 var errSeeker
= errors
.New("seeker can't seek")
138 // if name is empty, filename is unknown. (used for mime type, before sniffing)
139 // if modtime.IsZero(), modtime is unknown.
140 // content must be seeked to the beginning of the file.
141 // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
142 func serveContent(w ResponseWriter
, r
*Request
, name
string, modtime time
.Time
, sizeFunc
func() (int64, error
), content io
.ReadSeeker
) {
143 if checkLastModified(w
, r
, modtime
) {
146 rangeReq
, done
:= checkETag(w
, r
, modtime
)
153 // If Content-Type isn't set, use the file's extension to find it, but
154 // if the Content-Type is unset explicitly, do not sniff the type.
155 ctypes
, haveType
:= w
.Header()["Content-Type"]
158 ctype
= mime
.TypeByExtension(filepath
.Ext(name
))
160 // read a chunk to decide between utf-8 text and binary
161 var buf
[sniffLen
]byte
162 n
, _
:= io
.ReadFull(content
, buf
[:])
163 ctype
= DetectContentType(buf
[:n
])
164 _
, err
:= content
.Seek(0, os
.SEEK_SET
) // rewind to output whole file
166 Error(w
, "seeker can't seek", StatusInternalServerError
)
170 w
.Header().Set("Content-Type", ctype
)
171 } else if len(ctypes
) > 0 {
175 size
, err
:= sizeFunc()
177 Error(w
, err
.Error(), StatusInternalServerError
)
181 // handle Content-Range header.
183 var sendContent io
.Reader
= content
185 ranges
, err
:= parseRange(rangeReq
, size
)
187 Error(w
, err
.Error(), StatusRequestedRangeNotSatisfiable
)
190 if sumRangesSize(ranges
) > size
{
191 // The total number of bytes in all the ranges
192 // is larger than the size of the file by
193 // itself, so this is probably an attack, or a
194 // dumb client. Ignore the range request.
198 case len(ranges
) == 1:
199 // RFC 2616, Section 14.16:
200 // "When an HTTP message includes the content of a single
201 // range (for example, a response to a request for a
202 // single range, or to a request for a set of ranges
203 // that overlap without any holes), this content is
204 // transmitted with a Content-Range header, and a
205 // Content-Length header showing the number of bytes
206 // actually transferred.
208 // A response to a request for a single range MUST NOT
209 // be sent using the multipart/byteranges media type."
211 if _
, err
:= content
.Seek(ra
.start
, os
.SEEK_SET
); err
!= nil {
212 Error(w
, err
.Error(), StatusRequestedRangeNotSatisfiable
)
216 code
= StatusPartialContent
217 w
.Header().Set("Content-Range", ra
.contentRange(size
))
218 case len(ranges
) > 1:
219 sendSize
= rangesMIMESize(ranges
, ctype
, size
)
220 code
= StatusPartialContent
223 mw
:= multipart
.NewWriter(pw
)
224 w
.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw
.Boundary())
226 defer pr
.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
228 for _
, ra
:= range ranges
{
229 part
, err
:= mw
.CreatePart(ra
.mimeHeader(ctype
, size
))
231 pw
.CloseWithError(err
)
234 if _
, err
:= content
.Seek(ra
.start
, os
.SEEK_SET
); err
!= nil {
235 pw
.CloseWithError(err
)
238 if _
, err
:= io
.CopyN(part
, content
, ra
.length
); err
!= nil {
239 pw
.CloseWithError(err
)
248 w
.Header().Set("Accept-Ranges", "bytes")
249 if w
.Header().Get("Content-Encoding") == "" {
250 w
.Header().Set("Content-Length", strconv
.FormatInt(sendSize
, 10))
256 if r
.Method
!= "HEAD" {
257 io
.CopyN(w
, sendContent
, sendSize
)
261 // modtime is the modification time of the resource to be served, or IsZero().
262 // return value is whether this request is now complete.
263 func checkLastModified(w ResponseWriter
, r
*Request
, modtime time
.Time
) bool {
264 if modtime
.IsZero() {
268 // The Date-Modified header truncates sub-second precision, so
269 // use mtime < t+1s instead of mtime <= t to check for unmodified.
270 if t
, err
:= time
.Parse(TimeFormat
, r
.Header
.Get("If-Modified-Since")); err
== nil && modtime
.Before(t
.Add(1*time
.Second
)) {
272 delete(h
, "Content-Type")
273 delete(h
, "Content-Length")
274 w
.WriteHeader(StatusNotModified
)
277 w
.Header().Set("Last-Modified", modtime
.UTC().Format(TimeFormat
))
281 // checkETag implements If-None-Match and If-Range checks.
283 // The ETag or modtime must have been previously set in the
284 // ResponseWriter's headers. The modtime is only compared at second
285 // granularity and may be the zero value to mean unknown.
287 // The return value is the effective request "Range" header to use and
288 // whether this request is now considered done.
289 func checkETag(w ResponseWriter
, r
*Request
, modtime time
.Time
) (rangeReq
string, done
bool) {
290 etag
:= w
.Header().get("Etag")
291 rangeReq
= r
.Header
.get("Range")
293 // Invalidate the range request if the entity doesn't match the one
294 // the client was expecting.
295 // "If-Range: version" means "ignore the Range: header unless version matches the
297 // We only support ETag versions.
298 // The caller must have set the ETag on the response already.
299 if ir
:= r
.Header
.get("If-Range"); ir
!= "" && ir
!= etag
{
300 // The If-Range value is typically the ETag value, but it may also be
301 // the modtime date. See golang.org/issue/8367.
303 if !modtime
.IsZero() {
304 if t
, err
:= ParseTime(ir
); err
== nil && t
.Unix() == modtime
.Unix() {
313 if inm
:= r
.Header
.get("If-None-Match"); inm
!= "" {
316 return rangeReq
, false
319 // TODO(bradfitz): non-GET/HEAD requests require more work:
320 // sending a different status code on matches, and
321 // also can't use weak cache validators (those with a "W/
322 // prefix). But most users of ServeContent will be using
323 // it on GET or HEAD, so only support those for now.
324 if r
.Method
!= "GET" && r
.Method
!= "HEAD" {
325 return rangeReq
, false
328 // TODO(bradfitz): deal with comma-separated or multiple-valued
329 // list of If-None-match values. For now just handle the common
330 // case of a single item.
331 if inm
== etag || inm
== "*" {
333 delete(h
, "Content-Type")
334 delete(h
, "Content-Length")
335 w
.WriteHeader(StatusNotModified
)
339 return rangeReq
, false
342 // name is '/'-separated, not filepath.Separator.
343 func serveFile(w ResponseWriter
, r
*Request
, fs FileSystem
, name
string, redirect
bool) {
344 const indexPage
= "/index.html"
346 // redirect .../index.html to .../
347 // can't use Redirect() because that would make the path absolute,
348 // which would be a problem running under StripPrefix
349 if strings
.HasSuffix(r
.URL
.Path
, indexPage
) {
350 localRedirect(w
, r
, "./")
354 f
, err
:= fs
.Open(name
)
356 // TODO expose actual error?
364 // TODO expose actual error?
370 // redirect to canonical path: / at end of directory url
371 // r.URL.Path always begins with /
374 if url
[len(url
)-1] != '/' {
375 localRedirect(w
, r
, path
.Base(url
)+"/")
379 if url
[len(url
)-1] == '/' {
380 localRedirect(w
, r
, "../"+path
.Base(url
))
386 // use contents of index.html for directory, if present
388 index
:= strings
.TrimSuffix(name
, "/") + indexPage
389 ff
, err
:= fs
.Open(index
)
401 // Still a directory? (we didn't find an index.html file)
403 if checkLastModified(w
, r
, d
.ModTime()) {
410 // serveContent will check modification time
411 sizeFunc
:= func() (int64, error
) { return d
.Size(), nil }
412 serveContent(w
, r
, d
.Name(), d
.ModTime(), sizeFunc
, f
)
415 // localRedirect gives a Moved Permanently response.
416 // It does not convert relative paths to absolute paths like Redirect does.
417 func localRedirect(w ResponseWriter
, r
*Request
, newPath
string) {
418 if q
:= r
.URL
.RawQuery
; q
!= "" {
421 w
.Header().Set("Location", newPath
)
422 w
.WriteHeader(StatusMovedPermanently
)
425 // ServeFile replies to the request with the contents of the named file or directory.
426 func ServeFile(w ResponseWriter
, r
*Request
, name
string) {
427 dir
, file
:= filepath
.Split(name
)
428 serveFile(w
, r
, Dir(dir
), file
, false)
431 type fileHandler
struct {
435 // FileServer returns a handler that serves HTTP requests
436 // with the contents of the file system rooted at root.
438 // To use the operating system's file system implementation,
441 // http.Handle("/", http.FileServer(http.Dir("/tmp")))
442 func FileServer(root FileSystem
) Handler
{
443 return &fileHandler
{root
}
446 func (f
*fileHandler
) ServeHTTP(w ResponseWriter
, r
*Request
) {
448 if !strings
.HasPrefix(upath
, "/") {
452 serveFile(w
, r
, f
.root
, path
.Clean(upath
), true)
455 // httpRange specifies the byte range to be sent to the client.
456 type httpRange
struct {
460 func (r httpRange
) contentRange(size
int64) string {
461 return fmt
.Sprintf("bytes %d-%d/%d", r
.start
, r
.start
+r
.length
-1, size
)
464 func (r httpRange
) mimeHeader(contentType
string, size
int64) textproto
.MIMEHeader
{
465 return textproto
.MIMEHeader
{
466 "Content-Range": {r
.contentRange(size
)},
467 "Content-Type": {contentType
},
471 // parseRange parses a Range header string as per RFC 2616.
472 func parseRange(s
string, size
int64) ([]httpRange
, error
) {
474 return nil, nil // header not present
477 if !strings
.HasPrefix(s
, b
) {
478 return nil, errors
.New("invalid range")
480 var ranges
[]httpRange
481 for _
, ra
:= range strings
.Split(s
[len(b
):], ",") {
482 ra
= strings
.TrimSpace(ra
)
486 i
:= strings
.Index(ra
, "-")
488 return nil, errors
.New("invalid range")
490 start
, end
:= strings
.TrimSpace(ra
[:i
]), strings
.TrimSpace(ra
[i
+1:])
493 // If no start is specified, end specifies the
494 // range start relative to the end of the file.
495 i
, err
:= strconv
.ParseInt(end
, 10, 64)
497 return nil, errors
.New("invalid range")
503 r
.length
= size
- r
.start
505 i
, err
:= strconv
.ParseInt(start
, 10, 64)
506 if err
!= nil || i
> size || i
< 0 {
507 return nil, errors
.New("invalid range")
511 // If no end is specified, range extends to end of the file.
512 r
.length
= size
- r
.start
514 i
, err
:= strconv
.ParseInt(end
, 10, 64)
515 if err
!= nil || r
.start
> i
{
516 return nil, errors
.New("invalid range")
521 r
.length
= i
- r
.start
+ 1
524 ranges
= append(ranges
, r
)
529 // countingWriter counts how many bytes have been written to it.
530 type countingWriter
int64
532 func (w
*countingWriter
) Write(p
[]byte) (n
int, err error
) {
533 *w
+= countingWriter(len(p
))
537 // rangesMIMESize returns the number of bytes it takes to encode the
538 // provided ranges as a multipart response.
539 func rangesMIMESize(ranges
[]httpRange
, contentType
string, contentSize
int64) (encSize
int64) {
541 mw
:= multipart
.NewWriter(&w
)
542 for _
, ra
:= range ranges
{
543 mw
.CreatePart(ra
.mimeHeader(contentType
, contentSize
))
551 func sumRangesSize(ranges
[]httpRange
) (size
int64) {
552 for _
, ra
:= range ranges
{