// Package redoctober contains the server code for Red October. // // Copyright (c) 2013 CloudFlare, Inc. package main import ( "bytes" "crypto/tls" "crypto/x509" "encoding/pem" "errors" "flag" "fmt" "io" "io/ioutil" "log" "net" "net/http" "os" "strings" "time" "github.com/cloudflare/redoctober/config" "github.com/cloudflare/redoctober/core" "github.com/cloudflare/redoctober/report" "github.com/coreos/go-systemd/activation" "github.com/prometheus/client_golang/prometheus" ) // List of URLs to register and their related functions var functions = map[string]func([]byte) ([]byte, error){ "/create": core.Create, "/create-user": core.CreateUser, "/summary": core.Summary, "/purge": core.Purge, "/delegate": core.Delegate, "/password": core.Password, "/encrypt": core.Encrypt, "/re-encrypt": core.ReEncrypt, "/decrypt": core.Decrypt, "/owners": core.Owners, "/modify": core.Modify, "/export": core.Export, "/order": core.Order, "/orderout": core.OrdersOutstanding, "/orderinfo": core.OrderInfo, "/ordercancel": core.OrderCancel, "/restore": core.Restore, "/reset-persisted": core.ResetPersisted, "/status": core.Status, } type userRequest struct { rt string // The request type (which will be one of the // keys of the functions map above in []byte // Arbitrary input data (depends on the core.* // function called) resp chan<- []byte // Channel down which a response is sent (the // data sent will depend on the core.* function // called to handle this request) } // processRequest handles a single request receive on the JSON API for // one of the functions named in the functions map above. func processRequest(requestType string, w http.ResponseWriter, r *http.Request) { header := w.Header() header.Set("Content-Type", "application/json") header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload") tags := map[string]string{ "request-type": requestType, "request-from": r.RemoteAddr, } fn, ok := functions[requestType] if !ok { err := errors.New("redoctober: unknown request for " + requestType) report.Check(err, tags) http.Error(w, "Unknown request", http.StatusInternalServerError) return } body, err := ioutil.ReadAll(r.Body) if err != nil { report.Check(err, tags) http.Error(w, err.Error(), http.StatusInternalServerError) return } resp, err := fn(body) if err != nil { // The function should also report errors in more detail. report.Check(err, tags) log.Printf("http.main failed: %s: %s", requestType, err) http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(resp) } // NewServer starts an HTTPS server the handles the redoctober JSON // API. Each of the URIs in the functions map above is setup with a // separate HandleFunc. Each HandleFunc is an instance of queueRequest // above. // // Returns a valid http.Server handling redoctober JSON requests (and // its associated listener) or an error func NewServer(staticPath, addr, caPath string, certPaths, keyPaths []string, useSystemdSocket bool) (*http.Server, net.Listener, error) { config := &tls.Config{ PreferServerCipherSuites: true, SessionTicketsDisabled: true, } for i, certPath := range certPaths { cert, err := tls.LoadX509KeyPair(certPath, keyPaths[i]) if err != nil { return nil, nil, fmt.Errorf("Error loading certificate (%s, %s): %s", certPath, keyPaths[i], err) } config.Certificates = append(config.Certificates, cert) } config.BuildNameToCertificate() // If a caPath has been specified then a local CA is being used // and not the system configuration. if caPath != "" { pemCert, err := ioutil.ReadFile(caPath) if err != nil { return nil, nil, fmt.Errorf("Error reading %s: %s\n", caPath, err) } derCert, _ := pem.Decode(pemCert) if derCert == nil { return nil, nil, fmt.Errorf("No PEM data was found in the CA certificate file\n") } cert, err := x509.ParseCertificate(derCert.Bytes) if err != nil { return nil, nil, fmt.Errorf("Error parsing CA certificate: %s\n", err) } rootPool := x509.NewCertPool() rootPool.AddCert(cert) config.ClientAuth = tls.RequireAndVerifyClientCert config.ClientCAs = rootPool } var lstnr net.Listener if useSystemdSocket { listenFDs, err := activation.Listeners(true) if err != nil { log.Fatal(err) } if len(listenFDs) != 1 { log.Fatalf("Unexpected number of socket activation FDs! (%d)", len(listenFDs)) } lstnr = tls.NewListener(listenFDs[0], config) } else { conn, err := net.Listen("tcp", addr) if err != nil { return nil, nil, fmt.Errorf("Error starting TCP listener on %s: %s\n", addr, err) } lstnr = tls.NewListener(conn, config) } mux := http.NewServeMux() // queue up post URIs for current := range functions { // copy this so reference does not get overwritten requestType := current mux.HandleFunc(requestType, func(w http.ResponseWriter, r *http.Request) { log.Printf("http.server: endpoint=%s remote=%s", requestType, r.RemoteAddr) processRequest(requestType, w, r) }) } // queue up web frontend idxHandler := &indexHandler{staticPath} mux.HandleFunc("/index", idxHandler.handle) mux.HandleFunc("/", idxHandler.handle) srv := http.Server{ Addr: addr, Handler: mux, TLSConfig: config, TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, } return &srv, lstnr, nil } type indexHandler struct { staticPath string } func (this *indexHandler) handle(w http.ResponseWriter, r *http.Request) { var body io.ReadSeeker var tags = map[string]string{} if this.staticPath != "" { tags["static-path"] = this.staticPath f, err := os.Open(this.staticPath) if err != nil { report.Check(err, tags) http.Error(w, err.Error(), http.StatusInternalServerError) return } defer f.Close() body = f } else { body = bytes.NewReader([]byte(indexHtml)) } header := w.Header() header.Set("Content-Type", "text/html") header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload") // If the server isn't HTTPS worthy, the HSTS header won't be honored. http.ServeContent(w, r, "index.html", time.Now(), body) } // initPrometheus starts a goroutine with a Prometheus listener that // listens on localhost:metricsPort. If the Prometheus handler can't // be started, a log.Fatal call is made. func initPrometheus() { srv := &http.Server{ Addr: net.JoinHostPort(cfg.Metrics.Host, cfg.Metrics.Port), Handler: prometheus.Handler(), } log.Printf("metrics.init start: addr=%s", srv.Addr) go func() { err := srv.ListenAndServe() report.Check(err, nil) log.Fatal(err.Error()) }() } const usage = `Usage: redoctober -static -vaultpath -addr -certs [,,...] -keys [,,...] [-ca ] single-cert example: redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert.pem -keys cert.key multi-cert example: redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert1.pem,cert2.pem -keys cert1.key,cert2.key ` var ( cfg, cli *config.Config confFile string vaultPath string ) const ( defaultAddr = "localhost:8080" defaultMetricsHost = "localhost" defaultMetricsPort = "8081" ) func init() { // cli contains the configuration set by the command line // options, and cfg is the actual Red October config. cli = config.New() cfg = config.New() cli.Server.Addr = defaultAddr cli.Metrics.Host = defaultMetricsHost cli.Metrics.Port = defaultMetricsPort flag.Usage = func() { fmt.Fprint(os.Stderr, "main usage dump\n") fmt.Fprint(os.Stderr, usage) flag.PrintDefaults() os.Exit(2) } flag.StringVar(&confFile, "f", "", "path to config file") flag.StringVar(&cli.Server.Addr, "addr", cli.Server.Addr, "Server and port separated by :") flag.StringVar(&cli.Server.CAPath, "ca", cli.Server.CAPath, "Path of TLS CA for client authentication (optional)") flag.StringVar(&cli.Server.CertPaths, "certs", cli.Server.CertPaths, "Path(s) of TLS certificate in PEM format, comma-separated") flag.StringVar(&cli.HipChat.Host, "hchost", cli.HipChat.Host, "Hipchat Url Base (ex: hipchat.com)") flag.StringVar(&cli.HipChat.APIKey, "hckey", cli.HipChat.APIKey, "Hipchat API Key") flag.StringVar(&cli.HipChat.Room, "hcroom", cli.HipChat.Room, "Hipchat Room ID") flag.StringVar(&cli.Server.KeyPaths, "keys", cli.Server.KeyPaths, "Comma-separated list of PEM-encoded TLS private keys in the same order as certs") flag.StringVar(&cli.Metrics.Host, "metrics-host", cli.Metrics.Host, "The `host` the metrics endpoint should listen on.") flag.StringVar(&cli.Metrics.Port, "metrics-port", cli.Metrics.Port, "The `port` the metrics endpoint should listen on.") flag.StringVar(&cli.UI.Root, "rohost", cli.UI.Root, "RedOctober URL Base (ex: localhost:8080)") flag.StringVar(&cli.UI.Static, "static", cli.UI.Static, "Path to override built-in index.html") flag.BoolVar(&cli.Server.Systemd, "systemdfds", cli.Server.Systemd, "Use systemd socket activation to listen on a file. Useful for binding privileged sockets.") flag.StringVar(&vaultPath, "vaultpath", "diskrecord.json", "Path to the the disk vault") flag.Parse() } //go:generate go run generate.go func main() { var err error if confFile != "" { cfg, err = config.Load(confFile) if err != nil { log.Fatal(err) } } else { cfg = cli } report.Init(cfg) if vaultPath == "" || !cfg.Valid() { if !cfg.Valid() { fmt.Fprintf(os.Stderr, "Invalid config.\n") } fmt.Fprint(os.Stderr, usage) flag.PrintDefaults() os.Exit(2) } if err := core.Init(vaultPath, cfg); err != nil { report.Check(err, nil) log.Fatal(err) } initPrometheus() cpaths := strings.Split(cfg.Server.CertPaths, ",") kpaths := strings.Split(cfg.Server.KeyPaths, ",") s, l, err := NewServer(cfg.UI.Static, cfg.Server.Addr, cfg.Server.CAPath, cpaths, kpaths, cfg.Server.Systemd) if err != nil { report.Check(err, nil) log.Fatalf("Error starting redoctober server: %s\n", err) } log.Printf("http.serve start: addr=%s", cfg.Server.Addr) report.Recover(func() { err := s.Serve(l) report.Check(err, nil) log.Fatal(err.Error()) }) }