blob: 95c12d79aa83999332d9482c55505be75bc15c05 [file] [log] [blame]
// Package devserver provides code shared between Bazel and Blaze.
package devserver
import (
"bytes"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
// Convert Windows paths separators.
var pathReplacer = strings.NewReplacer("\\", "/")
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 := pathReplacer.Replace(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
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) {
// search through pkgs for the first index.html file found if any exists
for _, pkg := range pkgs {
// defaultIndex is not cached, so that a user's edits will be reflected.
defaultIndex := pathReplacer.Replace(filepath.Join(base, pkg, "index.html"))
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 := pathReplacer.Replace(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
}