github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/src/pkg/net/http/fs.go (about) 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 "os" 17 "path" 18 "path/filepath" 19 "strconv" 20 "strings" 21 "time" 22 ) 23 24 // A Dir implements http.FileSystem using the native file 25 // system restricted to a specific directory tree. 26 // 27 // An empty Dir is treated as ".". 28 type Dir string 29 30 func (d Dir) Open(name string) (File, error) { 31 if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 || 32 strings.Contains(name, "\x00") { 33 return nil, errors.New("http: invalid character in file path") 34 } 35 dir := string(d) 36 if dir == "" { 37 dir = "." 38 } 39 f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))) 40 if err != nil { 41 return nil, err 42 } 43 return f, nil 44 } 45 46 // A FileSystem implements access to a collection of named files. 47 // The elements in a file path are separated by slash ('/', U+002F) 48 // characters, regardless of host operating system convention. 49 type FileSystem interface { 50 Open(name string) (File, error) 51 } 52 53 // A File is returned by a FileSystem's Open method and can be 54 // served by the FileServer implementation. 55 type File interface { 56 Close() error 57 Stat() (os.FileInfo, error) 58 Readdir(count int) ([]os.FileInfo, error) 59 Read([]byte) (int, error) 60 Seek(offset int64, whence int) (int64, error) 61 } 62 63 func dirList(w ResponseWriter, f File) { 64 w.Header().Set("Content-Type", "text/html; charset=utf-8") 65 fmt.Fprintf(w, "<pre>\n") 66 for { 67 dirs, err := f.Readdir(100) 68 if err != nil || len(dirs) == 0 { 69 break 70 } 71 for _, d := range dirs { 72 name := d.Name() 73 if d.IsDir() { 74 name += "/" 75 } 76 // TODO htmlescape 77 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name) 78 } 79 } 80 fmt.Fprintf(w, "</pre>\n") 81 } 82 83 // ServeContent replies to the request using the content in the 84 // provided ReadSeeker. The main benefit of ServeContent over io.Copy 85 // is that it handles Range requests properly, sets the MIME type, and 86 // handles If-Modified-Since requests. 87 // 88 // If the response's Content-Type header is not set, ServeContent 89 // first tries to deduce the type from name's file extension and, 90 // if that fails, falls back to reading the first block of the content 91 // and passing it to DetectContentType. 92 // The name is otherwise unused; in particular it can be empty and is 93 // never sent in the response. 94 // 95 // If modtime is not the zero time, ServeContent includes it in a 96 // Last-Modified header in the response. If the request includes an 97 // If-Modified-Since header, ServeContent uses modtime to decide 98 // whether the content needs to be sent at all. 99 // 100 // The content's Seek method must work: ServeContent uses 101 // a seek to the end of the content to determine its size. 102 // 103 // If the caller has set w's ETag header, ServeContent uses it to 104 // handle requests using If-Range and If-None-Match. 105 // 106 // Note that *os.File implements the io.ReadSeeker interface. 107 func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) { 108 size, err := content.Seek(0, os.SEEK_END) 109 if err != nil { 110 Error(w, "seeker can't seek", StatusInternalServerError) 111 return 112 } 113 _, err = content.Seek(0, os.SEEK_SET) 114 if err != nil { 115 Error(w, "seeker can't seek", StatusInternalServerError) 116 return 117 } 118 serveContent(w, req, name, modtime, size, content) 119 } 120 121 // if name is empty, filename is unknown. (used for mime type, before sniffing) 122 // if modtime.IsZero(), modtime is unknown. 123 // content must be seeked to the beginning of the file. 124 func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) { 125 if checkLastModified(w, r, modtime) { 126 return 127 } 128 rangeReq, done := checkETag(w, r) 129 if done { 130 return 131 } 132 133 code := StatusOK 134 135 // If Content-Type isn't set, use the file's extension to find it. 136 ctype := w.Header().Get("Content-Type") 137 if ctype == "" { 138 ctype = mime.TypeByExtension(filepath.Ext(name)) 139 if ctype == "" { 140 // read a chunk to decide between utf-8 text and binary 141 var buf [1024]byte 142 n, _ := io.ReadFull(content, buf[:]) 143 b := buf[:n] 144 ctype = DetectContentType(b) 145 _, err := content.Seek(0, os.SEEK_SET) // rewind to output whole file 146 if err != nil { 147 Error(w, "seeker can't seek", StatusInternalServerError) 148 return 149 } 150 } 151 w.Header().Set("Content-Type", ctype) 152 } 153 154 // handle Content-Range header. 155 sendSize := size 156 var sendContent io.Reader = content 157 if size >= 0 { 158 ranges, err := parseRange(rangeReq, size) 159 if err != nil { 160 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) 161 return 162 } 163 if sumRangesSize(ranges) >= size { 164 // The total number of bytes in all the ranges 165 // is larger than the size of the file by 166 // itself, so this is probably an attack, or a 167 // dumb client. Ignore the range request. 168 ranges = nil 169 } 170 switch { 171 case len(ranges) == 1: 172 // RFC 2616, Section 14.16: 173 // "When an HTTP message includes the content of a single 174 // range (for example, a response to a request for a 175 // single range, or to a request for a set of ranges 176 // that overlap without any holes), this content is 177 // transmitted with a Content-Range header, and a 178 // Content-Length header showing the number of bytes 179 // actually transferred. 180 // ... 181 // A response to a request for a single range MUST NOT 182 // be sent using the multipart/byteranges media type." 183 ra := ranges[0] 184 if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil { 185 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) 186 return 187 } 188 sendSize = ra.length 189 code = StatusPartialContent 190 w.Header().Set("Content-Range", ra.contentRange(size)) 191 case len(ranges) > 1: 192 for _, ra := range ranges { 193 if ra.start > size { 194 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) 195 return 196 } 197 } 198 sendSize = rangesMIMESize(ranges, ctype, size) 199 code = StatusPartialContent 200 201 pr, pw := io.Pipe() 202 mw := multipart.NewWriter(pw) 203 w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary()) 204 sendContent = pr 205 defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish. 206 go func() { 207 for _, ra := range ranges { 208 part, err := mw.CreatePart(ra.mimeHeader(ctype, size)) 209 if err != nil { 210 pw.CloseWithError(err) 211 return 212 } 213 if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil { 214 pw.CloseWithError(err) 215 return 216 } 217 if _, err := io.CopyN(part, content, ra.length); err != nil { 218 pw.CloseWithError(err) 219 return 220 } 221 } 222 mw.Close() 223 pw.Close() 224 }() 225 } 226 227 w.Header().Set("Accept-Ranges", "bytes") 228 if w.Header().Get("Content-Encoding") == "" { 229 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10)) 230 } 231 } 232 233 w.WriteHeader(code) 234 235 if r.Method != "HEAD" { 236 io.CopyN(w, sendContent, sendSize) 237 } 238 } 239 240 // modtime is the modification time of the resource to be served, or IsZero(). 241 // return value is whether this request is now complete. 242 func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool { 243 if modtime.IsZero() { 244 return false 245 } 246 247 // The Date-Modified header truncates sub-second precision, so 248 // use mtime < t+1s instead of mtime <= t to check for unmodified. 249 if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) { 250 h := w.Header() 251 delete(h, "Content-Type") 252 delete(h, "Content-Length") 253 w.WriteHeader(StatusNotModified) 254 return true 255 } 256 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat)) 257 return false 258 } 259 260 // checkETag implements If-None-Match and If-Range checks. 261 // The ETag must have been previously set in the ResponseWriter's headers. 262 // 263 // The return value is the effective request "Range" header to use and 264 // whether this request is now considered done. 265 func checkETag(w ResponseWriter, r *Request) (rangeReq string, done bool) { 266 etag := w.Header().get("Etag") 267 rangeReq = r.Header.get("Range") 268 269 // Invalidate the range request if the entity doesn't match the one 270 // the client was expecting. 271 // "If-Range: version" means "ignore the Range: header unless version matches the 272 // current file." 273 // We only support ETag versions. 274 // The caller must have set the ETag on the response already. 275 if ir := r.Header.get("If-Range"); ir != "" && ir != etag { 276 // TODO(bradfitz): handle If-Range requests with Last-Modified 277 // times instead of ETags? I'd rather not, at least for 278 // now. That seems like a bug/compromise in the RFC 2616, and 279 // I've never heard of anybody caring about that (yet). 280 rangeReq = "" 281 } 282 283 if inm := r.Header.get("If-None-Match"); inm != "" { 284 // Must know ETag. 285 if etag == "" { 286 return rangeReq, false 287 } 288 289 // TODO(bradfitz): non-GET/HEAD requests require more work: 290 // sending a different status code on matches, and 291 // also can't use weak cache validators (those with a "W/ 292 // prefix). But most users of ServeContent will be using 293 // it on GET or HEAD, so only support those for now. 294 if r.Method != "GET" && r.Method != "HEAD" { 295 return rangeReq, false 296 } 297 298 // TODO(bradfitz): deal with comma-separated or multiple-valued 299 // list of If-None-match values. For now just handle the common 300 // case of a single item. 301 if inm == etag || inm == "*" { 302 h := w.Header() 303 delete(h, "Content-Type") 304 delete(h, "Content-Length") 305 w.WriteHeader(StatusNotModified) 306 return "", true 307 } 308 } 309 return rangeReq, false 310 } 311 312 // name is '/'-separated, not filepath.Separator. 313 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) { 314 const indexPage = "/index.html" 315 316 // redirect .../index.html to .../ 317 // can't use Redirect() because that would make the path absolute, 318 // which would be a problem running under StripPrefix 319 if strings.HasSuffix(r.URL.Path, indexPage) { 320 localRedirect(w, r, "./") 321 return 322 } 323 324 f, err := fs.Open(name) 325 if err != nil { 326 // TODO expose actual error? 327 NotFound(w, r) 328 return 329 } 330 defer f.Close() 331 332 d, err1 := f.Stat() 333 if err1 != nil { 334 // TODO expose actual error? 335 NotFound(w, r) 336 return 337 } 338 339 if redirect { 340 // redirect to canonical path: / at end of directory url 341 // r.URL.Path always begins with / 342 url := r.URL.Path 343 if d.IsDir() { 344 if url[len(url)-1] != '/' { 345 localRedirect(w, r, path.Base(url)+"/") 346 return 347 } 348 } else { 349 if url[len(url)-1] == '/' { 350 localRedirect(w, r, "../"+path.Base(url)) 351 return 352 } 353 } 354 } 355 356 // use contents of index.html for directory, if present 357 if d.IsDir() { 358 index := name + indexPage 359 ff, err := fs.Open(index) 360 if err == nil { 361 defer ff.Close() 362 dd, err := ff.Stat() 363 if err == nil { 364 name = index 365 d = dd 366 f = ff 367 } 368 } 369 } 370 371 // Still a directory? (we didn't find an index.html file) 372 if d.IsDir() { 373 if checkLastModified(w, r, d.ModTime()) { 374 return 375 } 376 dirList(w, f) 377 return 378 } 379 380 // serverContent will check modification time 381 serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f) 382 } 383 384 // localRedirect gives a Moved Permanently response. 385 // It does not convert relative paths to absolute paths like Redirect does. 386 func localRedirect(w ResponseWriter, r *Request, newPath string) { 387 if q := r.URL.RawQuery; q != "" { 388 newPath += "?" + q 389 } 390 w.Header().Set("Location", newPath) 391 w.WriteHeader(StatusMovedPermanently) 392 } 393 394 // ServeFile replies to the request with the contents of the named file or directory. 395 func ServeFile(w ResponseWriter, r *Request, name string) { 396 dir, file := filepath.Split(name) 397 serveFile(w, r, Dir(dir), file, false) 398 } 399 400 type fileHandler struct { 401 root FileSystem 402 } 403 404 // FileServer returns a handler that serves HTTP requests 405 // with the contents of the file system rooted at root. 406 // 407 // To use the operating system's file system implementation, 408 // use http.Dir: 409 // 410 // http.Handle("/", http.FileServer(http.Dir("/tmp"))) 411 func FileServer(root FileSystem) Handler { 412 return &fileHandler{root} 413 } 414 415 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { 416 upath := r.URL.Path 417 if !strings.HasPrefix(upath, "/") { 418 upath = "/" + upath 419 r.URL.Path = upath 420 } 421 serveFile(w, r, f.root, path.Clean(upath), true) 422 } 423 424 // httpRange specifies the byte range to be sent to the client. 425 type httpRange struct { 426 start, length int64 427 } 428 429 func (r httpRange) contentRange(size int64) string { 430 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size) 431 } 432 433 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader { 434 return textproto.MIMEHeader{ 435 "Content-Range": {r.contentRange(size)}, 436 "Content-Type": {contentType}, 437 } 438 } 439 440 // parseRange parses a Range header string as per RFC 2616. 441 func parseRange(s string, size int64) ([]httpRange, error) { 442 if s == "" { 443 return nil, nil // header not present 444 } 445 const b = "bytes=" 446 if !strings.HasPrefix(s, b) { 447 return nil, errors.New("invalid range") 448 } 449 var ranges []httpRange 450 for _, ra := range strings.Split(s[len(b):], ",") { 451 ra = strings.TrimSpace(ra) 452 if ra == "" { 453 continue 454 } 455 i := strings.Index(ra, "-") 456 if i < 0 { 457 return nil, errors.New("invalid range") 458 } 459 start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:]) 460 var r httpRange 461 if start == "" { 462 // If no start is specified, end specifies the 463 // range start relative to the end of the file. 464 i, err := strconv.ParseInt(end, 10, 64) 465 if err != nil { 466 return nil, errors.New("invalid range") 467 } 468 if i > size { 469 i = size 470 } 471 r.start = size - i 472 r.length = size - r.start 473 } else { 474 i, err := strconv.ParseInt(start, 10, 64) 475 if err != nil || i > size || i < 0 { 476 return nil, errors.New("invalid range") 477 } 478 r.start = i 479 if end == "" { 480 // If no end is specified, range extends to end of the file. 481 r.length = size - r.start 482 } else { 483 i, err := strconv.ParseInt(end, 10, 64) 484 if err != nil || r.start > i { 485 return nil, errors.New("invalid range") 486 } 487 if i >= size { 488 i = size - 1 489 } 490 r.length = i - r.start + 1 491 } 492 } 493 ranges = append(ranges, r) 494 } 495 return ranges, nil 496 } 497 498 // countingWriter counts how many bytes have been written to it. 499 type countingWriter int64 500 501 func (w *countingWriter) Write(p []byte) (n int, err error) { 502 *w += countingWriter(len(p)) 503 return len(p), nil 504 } 505 506 // rangesMIMESize returns the nunber of bytes it takes to encode the 507 // provided ranges as a multipart response. 508 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) { 509 var w countingWriter 510 mw := multipart.NewWriter(&w) 511 for _, ra := range ranges { 512 mw.CreatePart(ra.mimeHeader(contentType, contentSize)) 513 encSize += ra.length 514 } 515 mw.Close() 516 encSize += int64(w) 517 return 518 } 519 520 func sumRangesSize(ranges []httpRange) (size int64) { 521 for _, ra := range ranges { 522 size += ra.length 523 } 524 return 525 }