dashboard/buildlet: optional TLS + password support

Change-Id: Id72301c1be8da12d2c31cbec6cc94f26dc5ad808
Reviewed-on: https://go-review.googlesource.com/2743
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/buildlet/.gitignore b/buildlet/.gitignore
index 3775742..bbd21a2 100644
--- a/buildlet/.gitignore
+++ b/buildlet/.gitignore
@@ -1,3 +1,5 @@
 buildlet
 buildlet.*-*
 stage0/buildlet-stage0.*
+cert.pem
+key.pem
diff --git a/buildlet/README b/buildlet/README
new file mode 100644
index 0000000..0dd68cf
--- /dev/null
+++ b/buildlet/README
@@ -0,0 +1,12 @@
+Local development notes:
+
+Server:  (TLS stuff is optional)
+$ go run $GOROOT/src/crypto/tls/generate_cert.go --host=example.com
+$ GCEMETA_password=foo GCEMETA_tls_cert=@cert.pem GCEMETA_tls_key='@key.pem' ./buildlet
+
+Client:
+$ curl -O https://go.googlesource.com/go/+archive/3b76b017cabb.tar.gz
+$ curl -k --user :foo -X PUT --data-binary "@go-3b76b017cabb.tar.gz" https://localhost:5936/writetgz
+$ curl -k --user :foo -d "cmd=src/make.bash" http://127.0.0.1:5937/exec
+etc
+
diff --git a/buildlet/buildlet.go b/buildlet/buildlet.go
index e96f236..a2d911c 100644
--- a/buildlet/buildlet.go
+++ b/buildlet/buildlet.go
@@ -14,23 +14,16 @@
 // instances.
 package main // import "golang.org/x/tools/dashboard/buildlet"
 
-/* Notes:
-
-https://go.googlesource.com/go/+archive/3b76b017cabb.tar.gz
-curl -X PUT --data-binary "@go-3b76b017cabb.tar.gz" http://127.0.0.1:5937/writetgz
-
-curl -d "cmd=src/make.bash" http://127.0.0.1:5937/exec
-
-*/
-
 import (
 	"archive/tar"
 	"compress/gzip"
+	"crypto/tls"
 	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
+	"net"
 	"net/http"
 	"os"
 	"os/exec"
@@ -38,6 +31,7 @@
 	"runtime"
 	"strings"
 	"sync"
+	"time"
 
 	"google.golang.org/cloud/compute/metadata"
 )
@@ -54,11 +48,15 @@
 		// root).
 		return ":5936"
 	}
-	if metadata.OnGCE() {
-		// In production, default to
-		return ":80"
+	if !metadata.OnGCE() {
+		return "localhost:5936"
 	}
-	return "localhost:5936"
+	// In production, default to port 80 or 443, depending on
+	// whether TLS is configured.
+	if metadataValue("tls-cert") != "" {
+		return ":443"
+	}
+	return ":80"
 }
 
 func main() {
@@ -86,16 +84,90 @@
 	if _, err := os.Lstat(*scratchDir); err != nil {
 		log.Fatalf("invalid --scratchdir %q: %v", *scratchDir, err)
 	}
-	http.HandleFunc("/writetgz", handleWriteTGZ)
-	http.HandleFunc("/exec", handleExec)
 	http.HandleFunc("/", handleRoot)
+
+	password := metadataValue("password")
+	http.Handle("/writetgz", requirePassword{http.HandlerFunc(handleWriteTGZ), password})
+	http.Handle("/exec", requirePassword{http.HandlerFunc(handleExec), password})
 	// TODO: removeall
+
+	tlsCert, tlsKey := metadataValue("tls-cert"), metadataValue("tls-key")
+	if (tlsCert == "") != (tlsKey == "") {
+		log.Fatalf("tls-cert and tls-key must both be supplied, or neither.")
+	}
+
 	log.Printf("Listening on %s ...", *listenAddr)
-	log.Fatalf("ListenAndServe: %v", http.ListenAndServe(*listenAddr, nil))
+	ln, err := net.Listen("tcp", *listenAddr)
+	if err != nil {
+		log.Fatalf("Failed to listen on %s: %v", *listenAddr, err)
+	}
+	ln = tcpKeepAliveListener{ln.(*net.TCPListener)}
+
+	var srv http.Server
+	if tlsCert != "" {
+		cert, err := tls.X509KeyPair([]byte(tlsCert), []byte(tlsKey))
+		if err != nil {
+			log.Fatalf("TLS cert error: %v", err)
+		}
+		tlsConf := &tls.Config{
+			Certificates: []tls.Certificate{cert},
+		}
+		ln = tls.NewListener(ln, tlsConf)
+	}
+
+	log.Fatalf("Serve: %v", srv.Serve(ln))
+}
+
+// metadataValue returns the GCE metadata instance value for the given key.
+//
+// If not running on GCE, it falls back to using environment variables
+// for local development.
+func metadataValue(key string) string {
+	// The common case:
+	if metadata.OnGCE() {
+		v, err := metadata.InstanceAttributeValue(key)
+		if err != nil {
+			log.Fatalf("metadata.InstanceAttributeValue(%q): %v", key, err)
+		}
+		return v
+	}
+
+	// Else let developers use environment variables to fake
+	// metadata keys, for local testing.
+	envKey := "GCEMETA_" + strings.Replace(key, "-", "_", -1)
+	v := os.Getenv(envKey)
+	// Respect curl-style '@' prefix to mean the rest is a filename.
+	if strings.HasPrefix(v, "@") {
+		slurp, err := ioutil.ReadFile(v[1:])
+		if err != nil {
+			log.Fatalf("Error reading file for GCEMETA_%v: %v", key, err)
+		}
+		return string(slurp)
+	}
+	if v == "" {
+		log.Printf("Warning: not running on GCE, and no %v environment variable defined", envKey)
+	}
+	return v
+}
+
+// tcpKeepAliveListener is a net.Listener that sets TCP keep-alive
+// timeouts on accepted connections.
+type tcpKeepAliveListener struct {
+	*net.TCPListener
+}
+
+func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
+	tc, err := ln.AcceptTCP()
+	if err != nil {
+		return
+	}
+	tc.SetKeepAlive(true)
+	tc.SetKeepAlivePeriod(3 * time.Minute)
+	return tc, nil
 }
 
 func handleRoot(w http.ResponseWriter, r *http.Request) {
-	fmt.Fprintf(w, "buildlet running on %s-%s", runtime.GOOS, runtime.GOARCH)
+	fmt.Fprintf(w, "buildlet running on %s-%s\n", runtime.GOOS, runtime.GOARCH)
 }
 
 func handleWriteTGZ(w http.ResponseWriter, r *http.Request) {
@@ -257,3 +329,19 @@
 func badRequest(msg string) error {
 	return httpError{http.StatusBadRequest, msg}
 }
+
+// requirePassword is an http.Handler auth wrapper that enforces a
+// HTTP Basic password. The username is ignored.
+type requirePassword struct {
+	h        http.Handler
+	password string // empty means no password
+}
+
+func (h requirePassword) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	_, gotPass, _ := r.BasicAuth()
+	if h.password != "" && h.password != gotPass {
+		http.Error(w, "invalid password", http.StatusForbidden)
+		return
+	}
+	h.h.ServeHTTP(w, r)
+}