dashboard/buildlet: optional TLS + password support
Change-Id: Id72301c1be8da12d2c31cbec6cc94f26dc5ad808 Reviewed-on: https://go-review.googlesource.com/2743 Reviewed-by: Andrew Gerrand <adg@golang.org>
This commit is contained in:
parent
e0ba1bf74c
commit
44d7ecb402
|
@ -1,3 +1,5 @@
|
||||||
buildlet
|
buildlet
|
||||||
buildlet.*-*
|
buildlet.*-*
|
||||||
stage0/buildlet-stage0.*
|
stage0/buildlet-stage0.*
|
||||||
|
cert.pem
|
||||||
|
key.pem
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -14,23 +14,16 @@
|
||||||
// instances.
|
// instances.
|
||||||
package main // import "golang.org/x/tools/dashboard/buildlet"
|
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 (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"crypto/tls"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -38,6 +31,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"google.golang.org/cloud/compute/metadata"
|
"google.golang.org/cloud/compute/metadata"
|
||||||
)
|
)
|
||||||
|
@ -54,11 +48,15 @@ func defaultListenAddr() string {
|
||||||
// root).
|
// root).
|
||||||
return ":5936"
|
return ":5936"
|
||||||
}
|
}
|
||||||
if metadata.OnGCE() {
|
if !metadata.OnGCE() {
|
||||||
// In production, default to
|
return "localhost:5936"
|
||||||
return ":80"
|
|
||||||
}
|
}
|
||||||
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() {
|
func main() {
|
||||||
|
@ -86,16 +84,90 @@ func main() {
|
||||||
if _, err := os.Lstat(*scratchDir); err != nil {
|
if _, err := os.Lstat(*scratchDir); err != nil {
|
||||||
log.Fatalf("invalid --scratchdir %q: %v", *scratchDir, err)
|
log.Fatalf("invalid --scratchdir %q: %v", *scratchDir, err)
|
||||||
}
|
}
|
||||||
http.HandleFunc("/writetgz", handleWriteTGZ)
|
|
||||||
http.HandleFunc("/exec", handleExec)
|
|
||||||
http.HandleFunc("/", handleRoot)
|
http.HandleFunc("/", handleRoot)
|
||||||
|
|
||||||
|
password := metadataValue("password")
|
||||||
|
http.Handle("/writetgz", requirePassword{http.HandlerFunc(handleWriteTGZ), password})
|
||||||
|
http.Handle("/exec", requirePassword{http.HandlerFunc(handleExec), password})
|
||||||
// TODO: removeall
|
// 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.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) {
|
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) {
|
func handleWriteTGZ(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -257,3 +329,19 @@ func (he httpError) httpStatus() int { return he.statusCode }
|
||||||
func badRequest(msg string) error {
|
func badRequest(msg string) error {
|
||||||
return httpError{http.StatusBadRequest, msg}
|
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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue