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 <bradfitz@golang.org>
diff --git a/cmd/tip/README b/cmd/tip/README
index 9b7d966..b96c107 100644
--- a/cmd/tip/README
+++ b/cmd/tip/README
@@ -30,4 +30,3 @@
 
 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 81d1054..cb1bb09 100644
--- a/cmd/tip/tip.go
+++ b/cmd/tip/tip.go
@@ -56,15 +56,14 @@
 
 	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 @@
 		}
 		s := &http.Server{
 			Addr:      ":https",
+			Handler:   mux,
 			TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
 		}
 		go func() {
@@ -245,6 +245,13 @@
 	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 @@
 	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 0000000..878954d
--- /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)
+	}
+}