Source file src/pkg/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 "os"
15 "path"
16 "path/filepath"
17 "strconv"
18 "strings"
19 "time"
20 )
21
22 // A Dir implements http.FileSystem using the native file
23 // system restricted to a specific directory tree.
24 //
25 // An empty Dir is treated as ".".
26 type Dir string
27
28 func (d Dir) Open(name string) (File, error) {
29 if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 {
30 return nil, errors.New("http: invalid character in file path")
31 }
32 dir := string(d)
33 if dir == "" {
34 dir = "."
35 }
36 f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))))
37 if err != nil {
38 return nil, err
39 }
40 return f, nil
41 }
42
43 // A FileSystem implements access to a collection of named files.
44 // The elements in a file path are separated by slash ('/', U+002F)
45 // characters, regardless of host operating system convention.
46 type FileSystem interface {
47 Open(name string) (File, error)
48 }
49
50 // A File is returned by a FileSystem's Open method and can be
51 // served by the FileServer implementation.
52 type File interface {
53 Close() error
54 Stat() (os.FileInfo, error)
55 Readdir(count int) ([]os.FileInfo, error)
56 Read([]byte) (int, error)
57 Seek(offset int64, whence int) (int64, error)
58 }
59
60 func dirList(w ResponseWriter, f File) {
61 w.Header().Set("Content-Type", "text/html; charset=utf-8")
62 fmt.Fprintf(w, "<pre>\n")
63 for {
64 dirs, err := f.Readdir(100)
65 if err != nil || len(dirs) == 0 {
66 break
67 }
68 for _, d := range dirs {
69 name := d.Name()
70 if d.IsDir() {
71 name += "/"
72 }
73 // TODO htmlescape
74 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name)
75 }
76 }
77 fmt.Fprintf(w, "</pre>\n")
78 }
79
80 // ServeContent replies to the request using the content in the
81 // provided ReadSeeker. The main benefit of ServeContent over io.Copy
82 // is that it handles Range requests properly, sets the MIME type, and
83 // handles If-Modified-Since requests.
84 //
85 // If the response's Content-Type header is not set, ServeContent
86 // first tries to deduce the type from name's file extension and,
87 // if that fails, falls back to reading the first block of the content
88 // and passing it to DetectContentType.
89 // The name is otherwise unused; in particular it can be empty and is
90 // never sent in the response.
91 //
92 // If modtime is not the zero time, ServeContent includes it in a
93 // Last-Modified header in the response. If the request includes an
94 // If-Modified-Since header, ServeContent uses modtime to decide
95 // whether the content needs to be sent at all.
96 //
97 // The content's Seek method must work: ServeContent uses
98 // a seek to the end of the content to determine its size.
99 //
100 // Note that *os.File implements the io.ReadSeeker interface.
101 func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
102 size, err := content.Seek(0, os.SEEK_END)
103 if err != nil {
104 Error(w, "seeker can't seek", StatusInternalServerError)
105 return
106 }
107 _, err = content.Seek(0, os.SEEK_SET)
108 if err != nil {
109 Error(w, "seeker can't seek", StatusInternalServerError)
110 return
111 }
112 serveContent(w, req, name, modtime, size, content)
113 }
114
115 // if name is empty, filename is unknown. (used for mime type, before sniffing)
116 // if modtime.IsZero(), modtime is unknown.
117 // content must be seeked to the beginning of the file.
118 func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) {
119 if checkLastModified(w, r, modtime) {
120 return
121 }
122
123 code := StatusOK
124
125 // If Content-Type isn't set, use the file's extension to find it.
126 if w.Header().Get("Content-Type") == "" {
127 ctype := mime.TypeByExtension(filepath.Ext(name))
128 if ctype == "" {
129 // read a chunk to decide between utf-8 text and binary
130 var buf [1024]byte
131 n, _ := io.ReadFull(content, buf[:])
132 b := buf[:n]
133 ctype = DetectContentType(b)
134 _, err := content.Seek(0, os.SEEK_SET) // rewind to output whole file
135 if err != nil {
136 Error(w, "seeker can't seek", StatusInternalServerError)
137 return
138 }
139 }
140 w.Header().Set("Content-Type", ctype)
141 }
142
143 // handle Content-Range header.
144 // TODO(adg): handle multiple ranges
145 sendSize := size
146 if size >= 0 {
147 ranges, err := parseRange(r.Header.Get("Range"), size)
148 if err == nil && len(ranges) > 1 {
149 err = errors.New("multiple ranges not supported")
150 }
151 if err != nil {
152 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
153 return
154 }
155 if len(ranges) == 1 {
156 ra := ranges[0]
157 if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
158 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
159 return
160 }
161 sendSize = ra.length
162 code = StatusPartialContent
163 w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, size))
164 }
165
166 w.Header().Set("Accept-Ranges", "bytes")
167 if w.Header().Get("Content-Encoding") == "" {
168 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
169 }
170 }
171
172 w.WriteHeader(code)
173
174 if r.Method != "HEAD" {
175 if sendSize == -1 {
176 io.Copy(w, content)
177 } else {
178 io.CopyN(w, content, sendSize)
179 }
180 }
181 }
182
183 // modtime is the modification time of the resource to be served, or IsZero().
184 // return value is whether this request is now complete.
185 func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
186 if modtime.IsZero() {
187 return false
188 }
189
190 // The Date-Modified header truncates sub-second precision, so
191 // use mtime < t+1s instead of mtime <= t to check for unmodified.
192 if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
193 w.WriteHeader(StatusNotModified)
194 return true
195 }
196 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
197 return false
198 }
199
200 // name is '/'-separated, not filepath.Separator.
201 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
202 const indexPage = "/index.html"
203
204 // redirect .../index.html to .../
205 // can't use Redirect() because that would make the path absolute,
206 // which would be a problem running under StripPrefix
207 if strings.HasSuffix(r.URL.Path, indexPage) {
208 localRedirect(w, r, "./")
209 return
210 }
211
212 f, err := fs.Open(name)
213 if err != nil {
214 // TODO expose actual error?
215 NotFound(w, r)
216 return
217 }
218 defer f.Close()
219
220 d, err1 := f.Stat()
221 if err1 != nil {
222 // TODO expose actual error?
223 NotFound(w, r)
224 return
225 }
226
227 if redirect {
228 // redirect to canonical path: / at end of directory url
229 // r.URL.Path always begins with /
230 url := r.URL.Path
231 if d.IsDir() {
232 if url[len(url)-1] != '/' {
233 localRedirect(w, r, path.Base(url)+"/")
234 return
235 }
236 } else {
237 if url[len(url)-1] == '/' {
238 localRedirect(w, r, "../"+path.Base(url))
239 return
240 }
241 }
242 }
243
244 // use contents of index.html for directory, if present
245 if d.IsDir() {
246 if checkLastModified(w, r, d.ModTime()) {
247 return
248 }
249 index := name + indexPage
250 ff, err := fs.Open(index)
251 if err == nil {
252 defer ff.Close()
253 dd, err := ff.Stat()
254 if err == nil {
255 name = index
256 d = dd
257 f = ff
258 }
259 }
260 }
261
262 if d.IsDir() {
263 dirList(w, f)
264 return
265 }
266
267 serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f)
268 }
269
270 // localRedirect gives a Moved Permanently response.
271 // It does not convert relative paths to absolute paths like Redirect does.
272 func localRedirect(w ResponseWriter, r *Request, newPath string) {
273 if q := r.URL.RawQuery; q != "" {
274 newPath += "?" + q
275 }
276 w.Header().Set("Location", newPath)
277 w.WriteHeader(StatusMovedPermanently)
278 }
279
280 // ServeFile replies to the request with the contents of the named file or directory.
281 func ServeFile(w ResponseWriter, r *Request, name string) {
282 dir, file := filepath.Split(name)
283 serveFile(w, r, Dir(dir), file, false)
284 }
285
286 type fileHandler struct {
287 root FileSystem
288 }
289
290 // FileServer returns a handler that serves HTTP requests
291 // with the contents of the file system rooted at root.
292 //
293 // To use the operating system's file system implementation,
294 // use http.Dir:
295 //
296 // http.Handle("/", http.FileServer(http.Dir("/tmp")))
297 func FileServer(root FileSystem) Handler {
298 return &fileHandler{root}
299 }
300
301 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
302 upath := r.URL.Path
303 if !strings.HasPrefix(upath, "/") {
304 upath = "/" + upath
305 r.URL.Path = upath
306 }
307 serveFile(w, r, f.root, path.Clean(upath), true)
308 }
309
310 // httpRange specifies the byte range to be sent to the client.
311 type httpRange struct {
312 start, length int64
313 }
314
315 // parseRange parses a Range header string as per RFC 2616.
316 func parseRange(s string, size int64) ([]httpRange, error) {
317 if s == "" {
318 return nil, nil // header not present
319 }
320 const b = "bytes="
321 if !strings.HasPrefix(s, b) {
322 return nil, errors.New("invalid range")
323 }
324 var ranges []httpRange
325 for _, ra := range strings.Split(s[len(b):], ",") {
326 i := strings.Index(ra, "-")
327 if i < 0 {
328 return nil, errors.New("invalid range")
329 }
330 start, end := ra[:i], ra[i+1:]
331 var r httpRange
332 if start == "" {
333 // If no start is specified, end specifies the
334 // range start relative to the end of the file.
335 i, err := strconv.ParseInt(end, 10, 64)
336 if err != nil {
337 return nil, errors.New("invalid range")
338 }
339 if i > size {
340 i = size
341 }
342 r.start = size - i
343 r.length = size - r.start
344 } else {
345 i, err := strconv.ParseInt(start, 10, 64)
346 if err != nil || i > size || i < 0 {
347 return nil, errors.New("invalid range")
348 }
349 r.start = i
350 if end == "" {
351 // If no end is specified, range extends to end of the file.
352 r.length = size - r.start
353 } else {
354 i, err := strconv.ParseInt(end, 10, 64)
355 if err != nil || r.start > i {
356 return nil, errors.New("invalid range")
357 }
358 if i >= size {
359 i = size - 1
360 }
361 r.length = i - r.start + 1
362 }
363 }
364 ranges = append(ranges, r)
365 }
366 return ranges, nil
367 }