github.com/cortesi/devd@v0.0.0-20200427000907-c1a3bfba27d8/fileserver/fileserver.go (about) 1 // Package fileserver provides a filesystem HTTP handler, based on the built-in 2 // Go FileServer. Extensions include better directory listings, support for 3 // injection, better and use of Context. 4 package fileserver 5 6 import ( 7 "errors" 8 "fmt" 9 "html/template" 10 "io" 11 "mime" 12 "net/http" 13 "os" 14 "path" 15 "path/filepath" 16 "sort" 17 "strconv" 18 "strings" 19 "time" 20 21 "golang.org/x/net/context" 22 23 "github.com/cortesi/devd/inject" 24 "github.com/cortesi/devd/routespec" 25 "github.com/cortesi/termlog" 26 ) 27 28 const sniffLen = 512 29 30 func rawHeaderGet(h http.Header, key string) string { 31 if v := h[key]; len(v) > 0 { 32 return v[0] 33 } 34 return "" 35 } 36 37 // fileSlice implements sort.Interface, which allows to sort by file name with 38 // directories first. 39 type fileSlice []os.FileInfo 40 41 func (p fileSlice) Len() int { 42 return len(p) 43 } 44 45 func (p fileSlice) Less(i, j int) bool { 46 a, b := p[i], p[j] 47 if a.IsDir() && !b.IsDir() { 48 return true 49 } 50 if b.IsDir() && !a.IsDir() { 51 return false 52 } 53 if strings.HasPrefix(a.Name(), ".") && !strings.HasPrefix(b.Name(), ".") { 54 return false 55 } 56 if strings.HasPrefix(b.Name(), ".") && !strings.HasPrefix(a.Name(), ".") { 57 return true 58 } 59 return a.Name() < b.Name() 60 } 61 62 func (p fileSlice) Swap(i, j int) { 63 p[i], p[j] = p[j], p[i] 64 } 65 66 type dirData struct { 67 Version string 68 Name string 69 Files fileSlice 70 } 71 72 type fourohfourData struct { 73 Version string 74 } 75 76 func stripPrefix(prefix string, path string) string { 77 if prefix == "" { 78 return path 79 } 80 if p := strings.TrimPrefix(path, prefix); len(p) < len(path) { 81 return p 82 } 83 return path 84 } 85 86 // errSeeker is returned by ServeContent's sizeFunc when the content 87 // doesn't seek properly. The underlying Seeker's error text isn't 88 // included in the sizeFunc reply so it's not sent over HTTP to end 89 // users. 90 var errSeeker = errors.New("seeker can't seek") 91 92 // if name is empty, filename is unknown. (used for mime type, before sniffing) 93 // if modtime.IsZero(), modtime is unknown. 94 // content must be seeked to the beginning of the file. 95 // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. 96 func serveContent(ci inject.CopyInject, w http.ResponseWriter, r *http.Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) error { 97 if checkLastModified(w, r, modtime) { 98 return nil 99 } 100 done := checkETag(w, r) 101 if done { 102 return nil 103 } 104 105 code := http.StatusOK 106 107 // If Content-Type isn't set, use the file's extension to find it, but 108 // if the Content-Type is unset explicitly, do not sniff the type. 109 ctypes, haveType := w.Header()["Content-Type"] 110 var ctype string 111 if !haveType { 112 ctype = mime.TypeByExtension(filepath.Ext(name)) 113 if ctype == "" { 114 // read a chunk to decide between utf-8 text and binary 115 var buf [sniffLen]byte 116 n, _ := io.ReadFull(content, buf[:]) 117 ctype = http.DetectContentType(buf[:n]) 118 _, err := content.Seek(0, os.SEEK_SET) // rewind to output whole file 119 if err != nil { 120 http.Error(w, "seeker can't seek", http.StatusInternalServerError) 121 return err 122 } 123 } 124 w.Header().Set("Content-Type", ctype) 125 } else if len(ctypes) > 0 { 126 ctype = ctypes[0] 127 } 128 129 injector, err := ci.Sniff(content, ctype) 130 if err != nil { 131 http.Error(w, err.Error(), http.StatusInternalServerError) 132 return err 133 } 134 135 size, err := sizeFunc() 136 if err != nil { 137 http.Error(w, err.Error(), http.StatusInternalServerError) 138 return err 139 } 140 141 if injector.Found() { 142 size = size + int64(injector.Extra()) 143 } 144 145 if size >= 0 { 146 if w.Header().Get("Content-Encoding") == "" { 147 w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) 148 } 149 } 150 151 w.WriteHeader(code) 152 if r.Method != "HEAD" { 153 _, err := injector.Copy(w) 154 if err != nil { 155 return err 156 } 157 } 158 return nil 159 } 160 161 // modtime is the modification time of the resource to be served, or IsZero(). 162 // return value is whether this request is now complete. 163 func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool { 164 if modtime.IsZero() { 165 return false 166 } 167 168 // The Date-Modified header truncates sub-second precision, so 169 // use mtime < t+1s instead of mtime <= t to check for unmodified. 170 if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) { 171 h := w.Header() 172 delete(h, "Content-Type") 173 delete(h, "Content-Length") 174 w.WriteHeader(http.StatusNotModified) 175 return true 176 } 177 w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) 178 return false 179 } 180 181 // checkETag implements If-None-Match checks. 182 // The ETag must have been previously set in the ResponseWriter's headers. 183 // 184 // The return value is whether this request is now considered done. 185 func checkETag(w http.ResponseWriter, r *http.Request) (done bool) { 186 etag := rawHeaderGet(w.Header(), "Etag") 187 if inm := rawHeaderGet(r.Header, "If-None-Match"); inm != "" { 188 // Must know ETag. 189 if etag == "" { 190 return false 191 } 192 193 // TODO(bradfitz): non-GET/HEAD requests require more work: 194 // sending a different status code on matches, and 195 // also can't use weak cache validators (those with a "W/ 196 // prefix). But most users of ServeContent will be using 197 // it on GET or HEAD, so only support those for now. 198 if r.Method != "GET" && r.Method != "HEAD" { 199 return false 200 } 201 202 // TODO(bradfitz): deal with comma-separated or multiple-valued 203 // list of If-None-match values. For now just handle the common 204 // case of a single item. 205 if inm == etag || inm == "*" { 206 h := w.Header() 207 delete(h, "Content-Type") 208 delete(h, "Content-Length") 209 w.WriteHeader(http.StatusNotModified) 210 return true 211 } 212 } 213 return false 214 } 215 216 // localRedirect gives a Moved Permanently response. 217 // It does not convert relative paths to absolute paths like Redirect does. 218 func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) { 219 if q := r.URL.RawQuery; q != "" { 220 newPath += "?" + q 221 } 222 w.Header().Set("Location", newPath) 223 w.WriteHeader(http.StatusMovedPermanently) 224 } 225 226 // FileServer returns a handler that serves HTTP requests 227 // with the contents of the file system rooted at root. 228 // 229 // To use the operating system's file system implementation, 230 // use http.Dir: 231 // 232 // http.Handle("/", &fileserver.FileServer{Root: http.Dir("/tmp")}) 233 type FileServer struct { 234 Version string 235 Root http.FileSystem 236 Inject inject.CopyInject 237 Templates *template.Template 238 NotFoundRoutes []routespec.RouteSpec 239 Prefix string 240 } 241 242 func (fserver *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 243 fserver.ServeHTTPContext(context.Background(), w, r) 244 } 245 246 // ServeHTTPContext is like ServeHTTP, but with added context 247 func (fserver *FileServer) ServeHTTPContext( 248 ctx context.Context, w http.ResponseWriter, r *http.Request, 249 ) { 250 logger := termlog.FromContext(ctx) 251 logger.SayAs("debug", "debug fileserver: serving with FileServer...") 252 253 upath := stripPrefix(fserver.Prefix, r.URL.Path) 254 if !strings.HasPrefix(upath, "/") { 255 upath = "/" + upath 256 } 257 fserver.serveFile(logger, w, r, path.Clean(upath), true) 258 } 259 260 // Given a path and a "not found" over-ride specification, return an array of 261 // over-ride paths that should be considered for serving, in priority order. We 262 // assume that path is a sub-path above a certain root, and we never return 263 // paths that would fall outside this. 264 // 265 // We also sanity check file extensions to make sure that the expected file 266 // type matches what we serve. This prevents an over-ride for *.html files from 267 // serving up data when, say, a missing .png is requested. 268 func notFoundSearchPaths(pth string, spec string) []string { 269 var ret []string 270 if strings.HasPrefix(spec, "/") { 271 ret = []string{path.Clean(spec)} 272 } else { 273 for { 274 pth = path.Dir(pth) 275 if pth == "/" { 276 ret = append(ret, path.Join(pth, spec)) 277 break 278 } 279 ret = append(ret, path.Join(pth, spec)) 280 } 281 } 282 return ret 283 } 284 285 // Get the media type for an extension, via a MIME lookup, defaulting to 286 // "text/html". 287 func _getType(ext string) string { 288 typ := mime.TypeByExtension(ext) 289 if typ == "" { 290 return "text/html" 291 } 292 smime, _, err := mime.ParseMediaType(typ) 293 if err != nil { 294 return "text/html" 295 } 296 return smime 297 } 298 299 // Checks whether the incoming request has the same expected type as an 300 // over-ride specification. 301 func matchTypes(spec string, req string) bool { 302 smime := _getType(path.Ext(spec)) 303 rmime := _getType(path.Ext(req)) 304 if smime == rmime { 305 return true 306 } 307 return false 308 } 309 310 func (fserver *FileServer) serve404(w http.ResponseWriter) error { 311 d := fourohfourData{ 312 Version: fserver.Version, 313 } 314 err := fserver.Inject.ServeTemplate( 315 http.StatusNotFound, 316 w, 317 fserver.Templates.Lookup("404.html"), 318 &d, 319 ) 320 if err != nil { 321 return err 322 } 323 return nil 324 } 325 326 func (fserver *FileServer) dirList(logger termlog.Logger, w http.ResponseWriter, name string, f http.File) { 327 w.Header().Set("Cache-Control", "no-store, must-revalidate") 328 files, err := f.Readdir(0) 329 if err != nil { 330 logger.Shout("Error reading directory for listing: %s", err) 331 return 332 } 333 sortedFiles := fileSlice(files) 334 sort.Sort(sortedFiles) 335 data := dirData{ 336 Version: fserver.Version, 337 Name: name, 338 Files: sortedFiles, 339 } 340 err = fserver.Inject.ServeTemplate( 341 http.StatusOK, 342 w, 343 fserver.Templates.Lookup("dirlist.html"), 344 data, 345 ) 346 if err != nil { 347 logger.Shout("Failed to generate dir listing: %s", err) 348 } 349 } 350 351 func (fserver *FileServer) notFound( 352 logger termlog.Logger, 353 w http.ResponseWriter, 354 r *http.Request, 355 name string, 356 dir *http.File, 357 ) (err error) { 358 sm := http.NewServeMux() 359 seen := make(map[string]bool) 360 for _, nfr := range fserver.NotFoundRoutes { 361 seen[nfr.MuxMatch()] = true 362 sm.HandleFunc( 363 nfr.MuxMatch(), 364 func(nfr routespec.RouteSpec) func(w http.ResponseWriter, r *http.Request) { 365 return func(w http.ResponseWriter, r *http.Request) { 366 if matchTypes(nfr.Value, r.URL.Path) { 367 for _, pth := range notFoundSearchPaths(name, nfr.Value) { 368 next, err := fserver.serveNotFoundFile(w, r, pth) 369 if err != nil { 370 logger.Shout("Unable to serve not-found override: %s", err) 371 } 372 if !next { 373 return 374 } 375 } 376 } 377 err = fserver.serve404(w) 378 if err != nil { 379 logger.Shout("Internal error: %s", err) 380 } 381 } 382 }(nfr), 383 ) 384 } 385 if _, exists := seen["/"]; !exists { 386 sm.HandleFunc( 387 "/", 388 func(response http.ResponseWriter, request *http.Request) { 389 if dir != nil { 390 d, err := (*dir).Stat() 391 if err != nil { 392 logger.Shout("Internal error: %s", err) 393 return 394 } 395 if checkLastModified(response, request, d.ModTime()) { 396 return 397 } 398 fserver.dirList(logger, response, name, *dir) 399 return 400 } 401 err = fserver.serve404(w) 402 if err != nil { 403 logger.Shout("Internal error: %s", err) 404 } 405 }, 406 ) 407 } 408 handle, _ := sm.Handler(r) 409 handle.ServeHTTP(w, r) 410 return err 411 } 412 413 // If the next return value is true, the caller should proceed to the next 414 // over-ride path if there is one. If the err return value is non-nil, serving 415 // should stop. 416 func (fserver *FileServer) serveNotFoundFile( 417 w http.ResponseWriter, 418 r *http.Request, 419 name string, 420 ) (next bool, err error) { 421 f, err := fserver.Root.Open(name) 422 if err != nil { 423 return true, nil 424 } 425 defer func() { _ = f.Close() }() 426 427 d, err := f.Stat() 428 if err != nil || d.IsDir() { 429 return true, nil 430 } 431 432 // serverContent will check modification time 433 sizeFunc := func() (int64, error) { return d.Size(), nil } 434 err = serveContent(fserver.Inject, w, r, d.Name(), d.ModTime(), sizeFunc, f) 435 if err != nil { 436 return false, fmt.Errorf("Error serving file: %s", err) 437 } 438 return false, nil 439 } 440 441 // name is '/'-separated, not filepath.Separator. 442 func (fserver *FileServer) serveFile( 443 logger termlog.Logger, 444 w http.ResponseWriter, 445 r *http.Request, 446 name string, 447 redirect bool, 448 ) { 449 const indexPage = "/index.html" 450 451 // redirect .../index.html to .../ 452 // can't use Redirect() because that would make the path absolute, 453 // which would be a problem running under StripPrefix 454 if strings.HasSuffix(r.URL.Path, indexPage) { 455 logger.SayAs( 456 "debug", "debug fileserver: redirecting %s -> ./", indexPage, 457 ) 458 localRedirect(w, r, "./") 459 return 460 } 461 462 f, err := fserver.Root.Open(name) 463 if err != nil { 464 logger.WarnAs("debug", "debug fileserver: %s", err) 465 if err := fserver.notFound(logger, w, r, name, nil); err != nil { 466 logger.Shout("Internal error: %s", err) 467 } 468 return 469 } 470 defer func() { _ = f.Close() }() 471 472 d, err1 := f.Stat() 473 if err1 != nil { 474 logger.WarnAs("debug", "debug fileserver: %s", err) 475 if err := fserver.notFound(logger, w, r, name, nil); err != nil { 476 logger.Shout("Internal error: %s", err) 477 } 478 return 479 } 480 481 if redirect { 482 // redirect to canonical path: / at end of directory url 483 url := r.URL.Path 484 if !strings.HasPrefix(url, "/") { 485 url = "/" + url 486 } 487 if d.IsDir() { 488 if url[len(url)-1] != '/' { 489 localRedirect(w, r, path.Base(url)+"/") 490 return 491 } 492 } else if url[len(url)-1] == '/' { 493 localRedirect(w, r, "../"+path.Base(url)) 494 return 495 } 496 } 497 498 // use contents of index.html for directory, if present 499 if d.IsDir() { 500 index := name + indexPage 501 ff, err := fserver.Root.Open(index) 502 if err == nil { 503 defer func() { _ = ff.Close() }() 504 dd, err := ff.Stat() 505 if err == nil { 506 name = index 507 d = dd 508 f = ff 509 } 510 } 511 } 512 513 // Still a directory? (we didn't find an index.html file) 514 if d.IsDir() { 515 if err := fserver.notFound(logger, w, r, name, &f); err != nil { 516 logger.Shout("Internal error: %s", err) 517 } 518 return 519 } 520 521 // serverContent will check modification time 522 sizeFunc := func() (int64, error) { return d.Size(), nil } 523 err = serveContent(fserver.Inject, w, r, d.Name(), d.ModTime(), sizeFunc, f) 524 if err != nil { 525 logger.Warn("Error serving file: %s", err) 526 } 527 }