From 37a1062ad0bb70f026cd45d6b92cb37eacd927bb Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Fri, 31 Mar 2017 16:31:41 -0700 Subject: [PATCH] cmd/tip: redirect http://tip.golang.org to https At some point we switched tip.golang.org to run in GKE, which terminates TLS directly on port 443. This requires a new technique for detecting a plain HTTP connection. In addition we may want to run talks.golang.org on App Engine Flex, which uses an X-Forwarded-Proto header to indicate HTTP, so let's prepare for that possibility. Fixes golang/go#19759. Change-Id: Iddc567214c5d28f61c405db065aa1b3f2c92fd85 Reviewed-on: https://go-review.googlesource.com/38800 Reviewed-by: Brad Fitzpatrick --- cmd/tip/README | 1 - cmd/tip/tip.go | 37 ++++++++++++++++++++++++++++++------- cmd/tip/tip_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 cmd/tip/tip_test.go diff --git a/cmd/tip/README b/cmd/tip/README index 9b7d9665..b96c1071 100644 --- a/cmd/tip/README +++ b/cmd/tip/README @@ -30,4 +30,3 @@ Kubernetes instructions: TODO(bradfitz): flesh out these instructions as I gain experience with updating this over time. Also: move talks.golang.org to GKE too? - diff --git a/cmd/tip/tip.go b/cmd/tip/tip.go index 81d1054d..cb1bb099 100644 --- a/cmd/tip/tip.go +++ b/cmd/tip/tip.go @@ -56,15 +56,14 @@ func main() { p := &Proxy{builder: b} go p.run() - http.Handle("/", httpsOnlyHandler{p}) - http.HandleFunc("/_ah/health", p.serveHealthCheck) + mux := newServeMux(p) log.Printf("Starting up tip server for builder %q", os.Getenv(k)) errc := make(chan error) go func() { - errc <- http.ListenAndServe(":8080", nil) + errc <- http.ListenAndServe(":8080", mux) }() if *autoCertDomain != "" { log.Printf("Listening on port 443 with LetsEncrypt support on domain %q", *autoCertDomain) @@ -74,6 +73,7 @@ func main() { } s := &http.Server{ Addr: ":https", + Handler: mux, TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, } go func() { @@ -245,6 +245,13 @@ func (p *Proxy) poll() { p.cmd = cmd } +func newServeMux(p *Proxy) http.Handler { + mux := http.NewServeMux() + mux.Handle("/", httpsOnlyHandler{p}) + mux.HandleFunc("/_ah/health", p.serveHealthCheck) + return mux +} + func waitReady(b Builder, hostport string) error { var err error deadline := time.Now().Add(startTimeout) @@ -360,20 +367,36 @@ func getOK(url string) (body []byte, err error) { return body, nil } -// httpsOnlyHandler redirects requests to "http://example.com/foo?bar" -// to "https://example.com/foo?bar" +// httpsOnlyHandler redirects requests to "http://example.com/foo?bar" to +// "https://example.com/foo?bar". It should be used when the server is listening +// for HTTP traffic behind a proxy that terminates TLS traffic, not when the Go +// server is terminating TLS directly. type httpsOnlyHandler struct { h http.Handler } +// isProxiedReq checks whether the server is running behind a proxy that may be +// terminating TLS. +func isProxiedReq(r *http.Request) bool { + if _, ok := r.Header["X-Appengine-Https"]; ok { + return true + } + if _, ok := r.Header["X-Forwarded-Proto"]; ok { + return true + } + return false +} + func (h httpsOnlyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("X-Appengine-Https") == "off" { + if r.Header.Get("X-Appengine-Https") == "off" || r.Header.Get("X-Forwarded-Proto") == "http" || + (!isProxiedReq(r) && r.TLS == nil) { r.URL.Scheme = "https" r.URL.Host = r.Host http.Redirect(w, r, r.URL.String(), http.StatusFound) return } - if r.Header.Get("X-Appengine-Https") == "on" { + if r.Header.Get("X-Appengine-Https") == "on" || r.Header.Get("X-Forwarded-Proto") == "https" || + (!isProxiedReq(r) && r.TLS != nil) { // Only set this header when we're actually in production. w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") } diff --git a/cmd/tip/tip_test.go b/cmd/tip/tip_test.go new file mode 100644 index 00000000..878954d5 --- /dev/null +++ b/cmd/tip/tip_test.go @@ -0,0 +1,25 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package main + +import ( + "net/http/httptest" + "testing" +) + +func TestTipRedirects(t *testing.T) { + mux := newServeMux(&Proxy{builder: &godocBuilder{}}) + req := httptest.NewRequest("GET", "http://example.com/foo?bar=baz", nil) + req.Header.Set("X-Forwarded-Proto", "http") + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + if w.Code != 302 { + t.Errorf("expected Code to be 302, got %d", w.Code) + } + want := "https://example.com/foo?bar=baz" + if loc := w.Header().Get("Location"); loc != want { + t.Errorf("Location header: got %s, want %s", loc, want) + } +}