Skip to content

Commit

Permalink
cmd/coordinator: expose multiple domains during local development
Browse files Browse the repository at this point in the history
The coordinator now serves both farmer.golang.org and build.golang.org.
It has been possible to access the build dashboard page during local
development either by modifying source code, or /etc/hosts, but this
is not convenient.

Instead, use a hostPathHandler in local development mode which maps:

	https://localhost:8119/farmer.golang.org/ → farmer.golang.org pages
	https://localhost:8119/build.golang.org/  → build.golang.org pages

Which is very convenient, and will become even better after the next CL
unifies the header across the pages.

This is based on the approach in golang.org/x/website/cmd/golangorg for
serving multiple domains on localhost, implemented in CL 328013 by Russ.

For golang/go#34744.

Change-Id: Icd90c776db0898a797455cbb0dfb0fc038d050d2
Reviewed-on: https://go-review.googlesource.com/c/build/+/340432
Run-TryBot: Dmitri Shuralyov <[email protected]>
TryBot-Result: Go Bot <[email protected]>
Trust: Dmitri Shuralyov <[email protected]>
Reviewed-by: Heschi Kreinick <[email protected]>
  • Loading branch information
dmitshur committed Aug 10, 2021
1 parent 6c10bed commit a3a479a
Showing 1 changed file with 80 additions and 4 deletions.
84 changes: 80 additions & 4 deletions cmd/coordinator/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,15 @@ func serveTLS(ln net.Listener) {
config.Certificates = []tls.Certificate{cert}
}

var h http.Handler = httpRouter{}
if *mode == "dev" {
// Use hostPathHandler in local development mode (only) to improve
// convenience of testing multiple domains that coordinator serves.
h = hostPathHandler(h)
}
server := &http.Server{
Addr: ln.Addr().String(),
Handler: httpRouter{},
Handler: h,
}
tlsLn := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config)
log.Printf("Coordinator serving on: %v", tlsLn.Addr())
Expand All @@ -222,10 +228,9 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
}

// httpRouter is the coordinator's mux, routing traffic to one of
// three locations:
// two locations:
// 1) a buildlet, from gomote clients (if X-Buildlet-Proxy is set)
// 2) our module proxy cache on GKE (if X-Proxy-Service == module-cache)
// 3) traffic to the coordinator itself (the default)
// 2) traffic to the coordinator itself (the default)
type httpRouter struct{}

func (httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand All @@ -236,6 +241,77 @@ func (httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.DefaultServeMux.ServeHTTP(w, r)
}

var validHosts = map[string]bool{
"farmer.golang.org": true,
"build.golang.org": true,
}

// hostPathHandler infers the host from the first element of the URL path,
// and rewrites URLs in the output HTML accordingly. It disables response
// compression to simplify the process of link rewriting.
func hostPathHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
elem, rest := strings.TrimPrefix(r.URL.Path, "/"), ""
if i := strings.Index(elem, "/"); i >= 0 {
elem, rest = elem[:i], elem[i+1:]
}
if !validHosts[elem] {
u := "/farmer.golang.org" + r.URL.EscapedPath()
if r.URL.RawQuery != "" {
u += "?" + r.URL.RawQuery
}
http.Redirect(w, r, u, http.StatusTemporaryRedirect)
return
}

r.Host = elem
r.URL.Host = elem
r.URL.Path = "/" + rest
r.Header.Set("Accept-Encoding", "identity") // Disable compression for link rewriting.
lw := &linkRewriter{ResponseWriter: w, host: r.Host}
h.ServeHTTP(lw, r)
lw.Flush()
})
}

// A linkRewriter is a ResponseWriter that rewrites links in HTML output.
// It rewrites relative links /foo to be /host/foo, and it rewrites any link
// https://h/foo, where h is in validHosts, to be /h/foo. This corrects the
// links to have the right form for the test server.
type linkRewriter struct {
http.ResponseWriter
host string
buf []byte
ct string // content-type
}

func (r *linkRewriter) Write(data []byte) (int, error) {
if r.ct == "" {
ct := r.Header().Get("Content-Type")
if ct == "" {
// Note: should use first 512 bytes, but first write is fine for our purposes.
ct = http.DetectContentType(data)
}
r.ct = ct
}
if !strings.HasPrefix(r.ct, "text/html") {
return r.ResponseWriter.Write(data)
}
r.buf = append(r.buf, data...)
return len(data), nil
}

func (r *linkRewriter) Flush() {
repl := []string{
`href="/`, `href="/` + r.host + `/`,
}
for host := range validHosts {
repl = append(repl, `href="https://`+host, `href="/`+host)
}
strings.NewReplacer(repl...).WriteString(r.ResponseWriter, string(r.buf))
r.buf = nil
}

// autocertManager is non-nil if LetsEncrypt is in use.
var autocertManager *autocert.Manager

Expand Down

0 comments on commit a3a479a

Please sign in to comment.