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 http.FileSystem using the native file
26 // system restricted to a specific directory tree.
28 // An empty Dir is treated as ".".
31 func (d Dir
) Open(name
string) (File
, error
) {
32 if filepath
.Separator
!= '/' && strings
.IndexRune(name
, filepath
.Separator
) >= 0 ||
33 strings
.Contains(name
, "\x00") {
34 return nil, errors
.New("http: invalid character in file path")
40 f
, err
:= os
.Open(filepath
.Join(dir
, filepath
.FromSlash(path
.Clean("/"+name
))))
47 // A FileSystem implements access to a collection of named files.
48 // The elements in a file path are separated by slash ('/', U+002F)
49 // characters, regardless of host operating system convention.
50 type FileSystem
interface {
51 Open(name
string) (File
, error
)
54 // A File is returned by a FileSystem's Open method and can be
55 // served by the FileServer implementation.
57 // The methods should behave the same as those on an *os.File.
61 Readdir(count
int) ([]os
.FileInfo
, error
)
62 Seek(offset
int64, whence
int) (int64, error
)
63 Stat() (os
.FileInfo
, error
)
66 func dirList(w ResponseWriter
, f File
) {
67 w
.Header().Set("Content-Type", "text/html; charset=utf-8")
68 fmt
.Fprintf(w
, "<pre>\n")
70 dirs
, err
:= f
.Readdir(100)
71 if err
!= nil ||
len(dirs
) == 0 {
74 for _
, d
:= range dirs
{
79 // name may contain '?' or '#', which must be escaped to remain
80 // part of the URL path, and not indicate the start of a query
81 // string or fragment.
82 url
:= url
.URL
{Path
: name
}
83 fmt
.Fprintf(w
, "<a href=\"%s\">%s</a>\n", url
.String(), htmlReplacer
.Replace(name
))
86 fmt
.Fprintf(w
, "</pre>\n")
89 // ServeContent replies to the request using the content in the
90 // provided ReadSeeker. The main benefit of ServeContent over io.Copy
91 // is that it handles Range requests properly, sets the MIME type, and
92 // handles If-Modified-Since requests.
94 // If the response's Content-Type header is not set, ServeContent
95 // first tries to deduce the type from name's file extension and,
96 // if that fails, falls back to reading the first block of the content
97 // and passing it to DetectContentType.
98 // The name is otherwise unused; in particular it can be empty and is
99 // never sent in the response.
101 // If modtime is not the zero time, ServeContent includes it in a
102 // Last-Modified header in the response. If the request includes an
103 // If-Modified-Since header, ServeContent uses modtime to decide
104 // whether the content needs to be sent at all.
106 // The content's Seek method must work: ServeContent uses
107 // a seek to the end of the content to determine its size.
109 // If the caller has set w's ETag header, ServeContent uses it to
110 // handle requests using If-Range and If-None-Match.
112 // Note that *os.File implements the io.ReadSeeker interface.
113 func ServeContent(w ResponseWriter
, req
*Request
, name
string, modtime time
.Time
, content io
.ReadSeeker
) {
114 sizeFunc
:= func() (int64, error
) {
115 size
, err
:= content
.Seek(0, os
.SEEK_END
)
119 _
, err
= content
.Seek(0, os
.SEEK_SET
)
125 serveContent(w
, req
, name
, modtime
, sizeFunc
, content
)
128 // errSeeker is returned by ServeContent's sizeFunc when the content
129 // doesn't seek properly. The underlying Seeker's error text isn't
130 // included in the sizeFunc reply so it's not sent over HTTP to end
132 var errSeeker
= errors
.New("seeker can't seek")
134 // if name is empty, filename is unknown. (used for mime type, before sniffing)
135 // if modtime.IsZero(), modtime is unknown.
136 // content must be seeked to the beginning of the file.
137 // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
138 func serveContent(w ResponseWriter
, r
*Request
, name
string, modtime time
.Time
, sizeFunc
func() (int64, error
), content io
.ReadSeeker
) {
139 if checkLastModified(w
, r
, modtime
) {
142 rangeReq
, done
:= checkETag(w
, r
)
149 // If Content-Type isn't set, use the file's extension to find it, but
150 // if the Content-Type is unset explicitly, do not sniff the type.
151 ctypes
, haveType
:= w
.Header()["Content-Type"]
154 ctype
= mime
.TypeByExtension(filepath
.Ext(name
))
156 // read a chunk to decide between utf-8 text and binary
157 var buf
[sniffLen
]byte
158 n
, _
:= io
.ReadFull(content
, buf
[:])
159 ctype
= DetectContentType(buf
[:n
])
160 _
, err
:= content
.Seek(0, os
.SEEK_SET
) // rewind to output whole file
162 Error(w
, "seeker can't seek", StatusInternalServerError
)
166 w
.Header().Set("Content-Type", ctype
)
167 } else if len(ctypes
) > 0 {
171 size
, err
:= sizeFunc()
173 Error(w
, err
.Error(), StatusInternalServerError
)
177 // handle Content-Range header.
179 var sendContent io
.Reader
= content
181 ranges
, err
:= parseRange(rangeReq
, size
)
183 Error(w
, err
.Error(), StatusRequestedRangeNotSatisfiable
)
186 if sumRangesSize(ranges
) > size
{
187 // The total number of bytes in all the ranges
188 // is larger than the size of the file by
189 // itself, so this is probably an attack, or a
190 // dumb client. Ignore the range request.
194 case len(ranges
) == 1:
195 // RFC 2616, Section 14.16:
196 // "When an HTTP message includes the content of a single
197 // range (for example, a response to a request for a
198 // single range, or to a request for a set of ranges
199 // that overlap without any holes), this content is
200 // transmitted with a Content-Range header, and a
201 // Content-Length header showing the number of bytes
202 // actually transferred.
204 // A response to a request for a single range MUST NOT
205 // be sent using the multipart/byteranges media type."
207 if _
, err
:= content
.Seek(ra
.start
, os
.SEEK_SET
); err
!= nil {
208 Error(w
, err
.Error(), StatusRequestedRangeNotSatisfiable
)
212 code
= StatusPartialContent
213 w
.Header().Set("Content-Range", ra
.contentRange(size
))
214 case len(ranges
) > 1:
215 for _
, ra
:= range ranges
{
217 Error(w
, err
.Error(), StatusRequestedRangeNotSatisfiable
)
221 sendSize
= rangesMIMESize(ranges
, ctype
, size
)
222 code
= StatusPartialContent
225 mw
:= multipart
.NewWriter(pw
)
226 w
.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw
.Boundary())
228 defer pr
.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
230 for _
, ra
:= range ranges
{
231 part
, err
:= mw
.CreatePart(ra
.mimeHeader(ctype
, size
))
233 pw
.CloseWithError(err
)
236 if _
, err
:= content
.Seek(ra
.start
, os
.SEEK_SET
); err
!= nil {
237 pw
.CloseWithError(err
)
240 if _
, err
:= io
.CopyN(part
, content
, ra
.length
); err
!= nil {
241 pw
.CloseWithError(err
)
250 w
.Header().Set("Accept-Ranges", "bytes")
251 if w
.Header().Get("Content-Encoding") == "" {
252 w
.Header().Set("Content-Length", strconv
.FormatInt(sendSize
, 10))
258 if r
.Method
!= "HEAD" {
259 io
.CopyN(w
, sendContent
, sendSize
)
263 // modtime is the modification time of the resource to be served, or IsZero().
264 // return value is whether this request is now complete.
265 func checkLastModified(w ResponseWriter
, r
*Request
, modtime time
.Time
) bool {
266 if modtime
.IsZero() {
270 // The Date-Modified header truncates sub-second precision, so
271 // use mtime < t+1s instead of mtime <= t to check for unmodified.
272 if t
, err
:= time
.Parse(TimeFormat
, r
.Header
.Get("If-Modified-Since")); err
== nil && modtime
.Before(t
.Add(1*time
.Second
)) {
274 delete(h
, "Content-Type")
275 delete(h
, "Content-Length")
276 w
.WriteHeader(StatusNotModified
)
279 w
.Header().Set("Last-Modified", modtime
.UTC().Format(TimeFormat
))
283 // checkETag implements If-None-Match and If-Range checks.
284 // The ETag must have been previously set in the ResponseWriter's headers.
286 // The return value is the effective request "Range" header to use and
287 // whether this request is now considered done.
288 func checkETag(w ResponseWriter
, r
*Request
) (rangeReq
string, done
bool) {
289 etag
:= w
.Header().get("Etag")
290 rangeReq
= r
.Header
.get("Range")
292 // Invalidate the range request if the entity doesn't match the one
293 // the client was expecting.
294 // "If-Range: version" means "ignore the Range: header unless version matches the
296 // We only support ETag versions.
297 // The caller must have set the ETag on the response already.
298 if ir
:= r
.Header
.get("If-Range"); ir
!= "" && ir
!= etag
{
299 // TODO(bradfitz): handle If-Range requests with Last-Modified
300 // times instead of ETags? I'd rather not, at least for
301 // now. That seems like a bug/compromise in the RFC 2616, and
302 // I've never heard of anybody caring about that (yet).
306 if inm
:= r
.Header
.get("If-None-Match"); inm
!= "" {
309 return rangeReq
, false
312 // TODO(bradfitz): non-GET/HEAD requests require more work:
313 // sending a different status code on matches, and
314 // also can't use weak cache validators (those with a "W/
315 // prefix). But most users of ServeContent will be using
316 // it on GET or HEAD, so only support those for now.
317 if r
.Method
!= "GET" && r
.Method
!= "HEAD" {
318 return rangeReq
, false
321 // TODO(bradfitz): deal with comma-separated or multiple-valued
322 // list of If-None-match values. For now just handle the common
323 // case of a single item.
324 if inm
== etag || inm
== "*" {
326 delete(h
, "Content-Type")
327 delete(h
, "Content-Length")
328 w
.WriteHeader(StatusNotModified
)
332 return rangeReq
, false
335 // name is '/'-separated, not filepath.Separator.
336 func serveFile(w ResponseWriter
, r
*Request
, fs FileSystem
, name
string, redirect
bool) {
337 const indexPage
= "/index.html"
339 // redirect .../index.html to .../
340 // can't use Redirect() because that would make the path absolute,
341 // which would be a problem running under StripPrefix
342 if strings
.HasSuffix(r
.URL
.Path
, indexPage
) {
343 localRedirect(w
, r
, "./")
347 f
, err
:= fs
.Open(name
)
349 // TODO expose actual error?
357 // TODO expose actual error?
363 // redirect to canonical path: / at end of directory url
364 // r.URL.Path always begins with /
367 if url
[len(url
)-1] != '/' {
368 localRedirect(w
, r
, path
.Base(url
)+"/")
372 if url
[len(url
)-1] == '/' {
373 localRedirect(w
, r
, "../"+path
.Base(url
))
379 // use contents of index.html for directory, if present
381 index
:= name
+ indexPage
382 ff
, err
:= fs
.Open(index
)
394 // Still a directory? (we didn't find an index.html file)
396 if checkLastModified(w
, r
, d
.ModTime()) {
403 // serverContent will check modification time
404 sizeFunc
:= func() (int64, error
) { return d
.Size(), nil }
405 serveContent(w
, r
, d
.Name(), d
.ModTime(), sizeFunc
, f
)
408 // localRedirect gives a Moved Permanently response.
409 // It does not convert relative paths to absolute paths like Redirect does.
410 func localRedirect(w ResponseWriter
, r
*Request
, newPath
string) {
411 if q
:= r
.URL
.RawQuery
; q
!= "" {
414 w
.Header().Set("Location", newPath
)
415 w
.WriteHeader(StatusMovedPermanently
)
418 // ServeFile replies to the request with the contents of the named file or directory.
419 func ServeFile(w ResponseWriter
, r
*Request
, name
string) {
420 dir
, file
:= filepath
.Split(name
)
421 serveFile(w
, r
, Dir(dir
), file
, false)
424 type fileHandler
struct {
428 // FileServer returns a handler that serves HTTP requests
429 // with the contents of the file system rooted at root.
431 // To use the operating system's file system implementation,
434 // http.Handle("/", http.FileServer(http.Dir("/tmp")))
435 func FileServer(root FileSystem
) Handler
{
436 return &fileHandler
{root
}
439 func (f
*fileHandler
) ServeHTTP(w ResponseWriter
, r
*Request
) {
441 if !strings
.HasPrefix(upath
, "/") {
445 serveFile(w
, r
, f
.root
, path
.Clean(upath
), true)
448 // httpRange specifies the byte range to be sent to the client.
449 type httpRange
struct {
453 func (r httpRange
) contentRange(size
int64) string {
454 return fmt
.Sprintf("bytes %d-%d/%d", r
.start
, r
.start
+r
.length
-1, size
)
457 func (r httpRange
) mimeHeader(contentType
string, size
int64) textproto
.MIMEHeader
{
458 return textproto
.MIMEHeader
{
459 "Content-Range": {r
.contentRange(size
)},
460 "Content-Type": {contentType
},
464 // parseRange parses a Range header string as per RFC 2616.
465 func parseRange(s
string, size
int64) ([]httpRange
, error
) {
467 return nil, nil // header not present
470 if !strings
.HasPrefix(s
, b
) {
471 return nil, errors
.New("invalid range")
473 var ranges
[]httpRange
474 for _
, ra
:= range strings
.Split(s
[len(b
):], ",") {
475 ra
= strings
.TrimSpace(ra
)
479 i
:= strings
.Index(ra
, "-")
481 return nil, errors
.New("invalid range")
483 start
, end
:= strings
.TrimSpace(ra
[:i
]), strings
.TrimSpace(ra
[i
+1:])
486 // If no start is specified, end specifies the
487 // range start relative to the end of the file.
488 i
, err
:= strconv
.ParseInt(end
, 10, 64)
490 return nil, errors
.New("invalid range")
496 r
.length
= size
- r
.start
498 i
, err
:= strconv
.ParseInt(start
, 10, 64)
499 if err
!= nil || i
> size || i
< 0 {
500 return nil, errors
.New("invalid range")
504 // If no end is specified, range extends to end of the file.
505 r
.length
= size
- r
.start
507 i
, err
:= strconv
.ParseInt(end
, 10, 64)
508 if err
!= nil || r
.start
> i
{
509 return nil, errors
.New("invalid range")
514 r
.length
= i
- r
.start
+ 1
517 ranges
= append(ranges
, r
)
522 // countingWriter counts how many bytes have been written to it.
523 type countingWriter
int64
525 func (w
*countingWriter
) Write(p
[]byte) (n
int, err error
) {
526 *w
+= countingWriter(len(p
))
530 // rangesMIMESize returns the number of bytes it takes to encode the
531 // provided ranges as a multipart response.
532 func rangesMIMESize(ranges
[]httpRange
, contentType
string, contentSize
int64) (encSize
int64) {
534 mw
:= multipart
.NewWriter(&w
)
535 for _
, ra
:= range ranges
{
536 mw
.CreatePart(ra
.mimeHeader(contentType
, contentSize
))
544 func sumRangesSize(ranges
[]httpRange
) (size
int64) {
545 for _
, ra
:= range ranges
{