blob: 01a04c36ddad0d45e6b3d701c6ac29d260d14f63 [file] [log] [blame]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"crypto/hmac"
"crypto/md5"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"golang.org/x/build"
)
func repeatDialCoordinator() {
for {
if err := dialCoordinator(); err != nil {
log.Print(err)
}
log.Printf("Waiting 30 seconds and dialing again.")
time.Sleep(30 * time.Second)
}
}
func dialCoordinator() error {
devMode := !strings.HasPrefix(*coordinator, "farmer.golang.org")
modes := strings.Split(*reverse, ",")
var keys []string
for _, m := range modes {
if devMode {
keys = append(keys, string(devBuilderKey(m)))
continue
}
keyPath := filepath.Join(homedir(), ".gobuildkey-"+m)
key, err := ioutil.ReadFile(keyPath)
if os.IsNotExist(err) && len(modes) == 1 {
globalKeyPath := filepath.Join(homedir(), ".gobuildkey")
key, err = ioutil.ReadFile(globalKeyPath)
if err != nil {
log.Fatalf("cannot read either key file %q or %q: %v", keyPath, globalKeyPath, err)
}
} else if err != nil {
log.Fatalf("cannot read key file %q: %v", keyPath, err)
}
keys = append(keys, string(key))
}
caCert := build.ProdCoordinatorCert
addr := *coordinator
if addr == "farmer.golang.org" {
addr = "farmer.golang.org:443"
}
if devMode {
caCert = build.DevCoordinatorCert
}
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM([]byte(caCert)) {
log.Fatal("failed to append coordinator CA certificate")
}
log.Printf("Dialing coordinator %s...", addr)
tcpConn, err := net.Dial("tcp", addr)
if err != nil {
return err // try again
}
config := &tls.Config{
ServerName: "go",
RootCAs: caPool,
}
conn := tls.Client(tcpConn, config)
if err := conn.Handshake(); err != nil {
return fmt.Errorf("failed to handshake with coordinator: %v", err)
}
bufr := bufio.NewReader(conn)
req, err := http.NewRequest("GET", "/reverse", nil)
if err != nil {
log.Fatal(err)
}
req.Header["X-Go-Builder-Type"] = modes
req.Header["X-Go-Builder-Key"] = keys
if err := req.Write(conn); err != nil {
return fmt.Errorf("coordinator /reverse request failed: %v", err)
}
resp, err := http.ReadResponse(bufr, req)
if err != nil {
return fmt.Errorf("coordinator /reverse response failed: %v", err)
}
if resp.StatusCode != 200 {
msg, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("coordinator registration failed:\n\t%s", msg)
}
resp.Body.Close()
// The client becomes the simple http server.
log.Printf("Connected to coordinator, serving HTTP back at them.")
stateCh := make(chan http.ConnState, 1)
srv := &http.Server{
ConnState: func(_ net.Conn, state http.ConnState) { stateCh <- state },
}
return srv.Serve(&reverseListener{
conn: conn,
stateCh: stateCh,
})
}
// reverseListener serves out a single underlying conn, once.
//
// It is designed to be passed to a *http.Server, which loops
// continually calling Accept. As this reverse connection only
// ever has one connection to hand out, it responds to the first
// Accept, and then blocks on the second Accept.
//
// While blocking on the second Accept, this listener takes on the
// job of checking the health of the original net.Conn it handed out.
// If it goes unused for a while, it closes the original net.Conn
// and returns an error, ending the life of the *http.Server
type reverseListener struct {
done bool
conn net.Conn
stateCh <-chan http.ConnState
}
func (rl *reverseListener) Accept() (net.Conn, error) {
if !rl.done {
// First call to Accept, return our one net.Conn.
rl.done = true
return rl.conn, nil
}
// Second call to Accept, block until we decide the entire
// server should be torn down.
defer rl.conn.Close()
const timeout = 1 * time.Minute
timer := time.NewTimer(timeout)
var state http.ConnState
for {
select {
case state = <-rl.stateCh:
if state == http.StateClosed {
return nil, errors.New("coordinator connection closed")
}
// The coordinator sends a health check every 30 seconds
// when buildlets are idle. If we go a minute without
// seeing anything, assume the coordinator is in a bad way
// (probably restarted) and close the connection.
case <-timer.C:
if state == http.StateIdle {
return nil, errors.New("coordinator connection unhealthy")
}
}
timer.Reset(timeout)
}
}
func (rl *reverseListener) Close() error { return nil }
func (rl *reverseListener) Addr() net.Addr { return reverseAddr("buildlet") }
// reverseAddr implements net.Addr for reverseListener.
type reverseAddr string
func (a reverseAddr) Network() string { return "reverse" }
func (a reverseAddr) String() string { return "reverse:" + string(a) }
func devBuilderKey(builder string) string {
h := hmac.New(md5.New, []byte("gophers rule"))
io.WriteString(h, builder)
return fmt.Sprintf("%x", h.Sum(nil))
}
func homedir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
return os.Getenv("HOME")
}
// TestDialCoordinator dials the coordinator. Exported for testing.
func TestDialCoordinator() {
// TODO(crawshaw): move some of this logic out of main to simplify testing hook.
http.Handle("/status", http.HandlerFunc(handleStatus))
dialCoordinator()
}