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>
This commit is contained in:
Kevin Burke 2017-03-31 16:31:41 -07:00 committed by Brad Fitzpatrick
parent 620ecdb8d7
commit 37a1062ad0
3 changed files with 55 additions and 8 deletions

View File

@ -30,4 +30,3 @@ Kubernetes instructions:
TODO(bradfitz): flesh out these instructions as I gain experience TODO(bradfitz): flesh out these instructions as I gain experience
with updating this over time. Also: move talks.golang.org to GKE too? with updating this over time. Also: move talks.golang.org to GKE too?

View File

@ -56,15 +56,14 @@ func main() {
p := &Proxy{builder: b} p := &Proxy{builder: b}
go p.run() go p.run()
http.Handle("/", httpsOnlyHandler{p}) mux := newServeMux(p)
http.HandleFunc("/_ah/health", p.serveHealthCheck)
log.Printf("Starting up tip server for builder %q", os.Getenv(k)) log.Printf("Starting up tip server for builder %q", os.Getenv(k))
errc := make(chan error) errc := make(chan error)
go func() { go func() {
errc <- http.ListenAndServe(":8080", nil) errc <- http.ListenAndServe(":8080", mux)
}() }()
if *autoCertDomain != "" { if *autoCertDomain != "" {
log.Printf("Listening on port 443 with LetsEncrypt support on domain %q", *autoCertDomain) log.Printf("Listening on port 443 with LetsEncrypt support on domain %q", *autoCertDomain)
@ -74,6 +73,7 @@ func main() {
} }
s := &http.Server{ s := &http.Server{
Addr: ":https", Addr: ":https",
Handler: mux,
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
} }
go func() { go func() {
@ -245,6 +245,13 @@ func (p *Proxy) poll() {
p.cmd = cmd 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 { func waitReady(b Builder, hostport string) error {
var err error var err error
deadline := time.Now().Add(startTimeout) deadline := time.Now().Add(startTimeout)
@ -360,20 +367,36 @@ func getOK(url string) (body []byte, err error) {
return body, nil return body, nil
} }
// httpsOnlyHandler redirects requests to "http://example.com/foo?bar" // httpsOnlyHandler redirects requests to "http://example.com/foo?bar" to
// to "https://example.com/foo?bar" // "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 { type httpsOnlyHandler struct {
h http.Handler 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) { 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.Scheme = "https"
r.URL.Host = r.Host r.URL.Host = r.Host
http.Redirect(w, r, r.URL.String(), http.StatusFound) http.Redirect(w, r, r.URL.String(), http.StatusFound)
return 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. // Only set this header when we're actually in production.
w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
} }

25
cmd/tip/tip_test.go Normal file
View File

@ -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)
}
}