| // Package devserver provides code shared between Bazel and Blaze. |
| package devserver |
| |
| import ( |
| "bytes" |
| "fmt" |
| "net" |
| "net/http" |
| "net/url" |
| "os" |
| "path/filepath" |
| "strings" |
| "time" |
| ) |
| |
| func shouldAllowCors(request *http.Request) bool { |
| hostname, err := os.Hostname() |
| if err != nil { |
| return false |
| } |
| referer, err := url.Parse(request.Header.Get("Origin")) |
| if err != nil { |
| return false |
| } |
| host, _, err := net.SplitHostPort(referer.Host) |
| // SplitHostPort fails when the parameter doesn't have a port. |
| if err != nil { |
| host = referer.Host |
| } |
| return host == hostname || host == "localhost" |
| } |
| |
| func customNotFoundMiddleware(notFound http.HandlerFunc, passThrough http.HandlerFunc) http.HandlerFunc { |
| return func(writer http.ResponseWriter, request *http.Request) { |
| passThrough( |
| &customNotFoundResponseWriter{ResponseWriter: writer, request: request, notFound: notFound}, |
| request, |
| ) |
| } |
| } |
| |
| type customNotFoundResponseWriter struct { |
| http.ResponseWriter |
| |
| request *http.Request |
| notFound http.HandlerFunc |
| has404 bool |
| hasWrite bool |
| } |
| |
| // Write implements http.ResponseWriter.Write. |
| func (w *customNotFoundResponseWriter) Write(b []byte) (int, error) { |
| w.hasWrite = true |
| if w.has404 { |
| // We have already written the not found response, so drop this one. |
| return len(b), nil |
| } |
| return w.ResponseWriter.Write(b) |
| } |
| |
| // WriteHeader implements http.ResponseWriter.WriteHeader. |
| func (w *customNotFoundResponseWriter) WriteHeader(code int) { |
| if code != http.StatusNotFound || w.hasWrite { |
| // We only intercept not found statuses. We also don't intercept statuses written after the |
| // first write as these are an error and should be handled by the default ResponseWriter. |
| w.ResponseWriter.WriteHeader(code) |
| return |
| } |
| |
| // WriteHeader writes out the entire header (including content type) and only the first call |
| // will succeed. Therefore, if we want the correct content type set, we must set it here. |
| w.Header().Del("Content-Type") |
| w.Header().Add("Content-Type", "text/html; charset=utf-8") |
| w.ResponseWriter.WriteHeader(code) |
| w.has404 = true |
| |
| // We have already written the header, so drop any calls to WriteHeader made by the not found |
| // handler. These additional calls are expected, and if passed through, would cause the base |
| // ResponseWriter to unnecessarily spam the error log. |
| w.notFound(&headerSuppressorResponseWriter{w.ResponseWriter}, w.request) |
| w.hasWrite = true |
| } |
| |
| type headerSuppressorResponseWriter struct { |
| http.ResponseWriter |
| } |
| |
| // WriteHeader implements http.ResponseWriter.WriteHeader. |
| func (w *headerSuppressorResponseWriter) WriteHeader(code int) {} |
| |
| // CreateFileHandler returns an http handler to locate files on disk |
| func CreateFileHandler(servingPath, manifest string, pkgs []string, base string) http.HandlerFunc { |
| pkgPaths := chainedDir{} |
| for _, pkg := range pkgs { |
| path := filepath.Join(base, pkg) |
| if _, err := os.Stat(path); err != nil { |
| fmt.Fprintf(os.Stderr, "Cannot read server root package at %s: %v\n", path, err) |
| os.Exit(1) |
| } |
| pkgPaths = append(pkgPaths, http.Dir(path)) |
| } |
| pkgPaths = append(pkgPaths, http.Dir(base)) |
| |
| fileHandler := http.FileServer(pkgPaths).ServeHTTP |
| |
| // defaultIndex is not cached, so that a user's edits will be reflected. |
| defaultIndex := filepath.Join(base, pkgs[0], "index.html") |
| defaultPage := []byte(fmt.Sprintf(`<!doctype html> |
| <html> |
| <head> |
| <title>ts_devserver (%s)</title> |
| </head> |
| <body> |
| <script src="%s"></script> |
| </body> |
| </html> |
| `, manifest, servingPath)) |
| |
| // indexHandler serves an index.html if present, or otherwise serves a minimal |
| // generated index.html with a script tag to include the bundled js source. |
| indexHandler := func(w http.ResponseWriter, r *http.Request) { |
| if _, err := os.Stat(defaultIndex); err == nil { |
| http.ServeFile(w, r, defaultIndex) |
| return |
| } |
| content := bytes.NewReader(defaultPage) |
| http.ServeContent(w, r, "index.html", time.Now(), content) |
| } |
| |
| // Serve a custom index.html so as to override the default directory listing |
| // from http.FileServer when no index.html file present. |
| indexOnNotFoundHandler := func(writer http.ResponseWriter, request *http.Request) { |
| // The browser can't tell the difference between different source checkouts or different devserver |
| // instances, so it may mistakenly cache static files (including templates) using versions from |
| // old instances if they haven't been modified more recently. To prevent this, we force no-cache |
| // on all static files. |
| writer.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") |
| if shouldAllowCors(request) { |
| writer.Header().Add("Access-Control-Allow-Origin", request.Header.Get("Origin")) |
| writer.Header().Add("Access-Control-Allow-Credentials", "true") |
| } |
| writer.Header().Add("Pragma", "no-cache") |
| writer.Header().Add("Expires", "0") |
| // Add gzip headers if serving .gz files. |
| if strings.HasSuffix(request.URL.EscapedPath(), ".gz") { |
| writer.Header().Add("Content-Encoding", "gzip") |
| } |
| |
| if request.URL.Path == "/" { |
| indexHandler(writer, request) |
| return |
| } |
| // When a file is not found, serve a 404 code but serve the index.html from above as its body. |
| // This allows applications to use html5 routing and reload the page at /some/sub/path, but still |
| // get their web app served. |
| writer = &customNotFoundResponseWriter{ResponseWriter: writer, request: request, notFound: indexHandler} |
| fileHandler(writer, request) |
| } |
| |
| return indexOnNotFoundHandler |
| } |
| |
| // chainedDir implements http.FileSystem by looking in the list of dirs one after each other. |
| type chainedDir []http.Dir |
| |
| func (chain chainedDir) Open(name string) (http.File, error) { |
| for _, dir := range chain { |
| f, err := dir.Open(name) |
| if os.IsNotExist(err) { |
| continue |
| } |
| if err != nil { |
| return nil, err |
| } |
| |
| // Do not return a directory, since FileServer will either: |
| // 1) serve the index.html file -or- |
| // 2) fall back to directory listings |
| // In place of (2), we prefer to fall back to our index.html. We accomplish |
| // this by lying to the FileServer that the directory doesn't exist. |
| stat, err := f.Stat() |
| if err != nil { |
| return nil, err |
| } |
| if stat.IsDir() { |
| // Make sure to close the previous file handle before moving to a different file. |
| f.Close() |
| indexName := filepath.Join(name, "index.html") |
| f, err := dir.Open(indexName) |
| if os.IsNotExist(err) { |
| continue |
| } |
| return f, err |
| } |
| |
| return f, nil |
| } |
| return nil, os.ErrNotExist |
| } |