Files
redoctober/redoctober.go
Kyle Isom cb16b159f3 Add a status endpoint to the server.
This pull request adds a status endpoint to the Red October server; as
of this pull request, the status endpoint only returns the current
delegation persistence state. The HTTP UI has not been updated, as
this is scoped out for a future request; however, the CLI utility now
features a status command to fetch this information.
2016-07-13 10:20:06 -07:00

308 lines
8.9 KiB
Go

// Package redoctober contains the server code for Red October.
//
// Copyright (c) 2013 CloudFlare, Inc.
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/core"
"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,
"/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")
fn, ok := functions[requestType]
if !ok {
http.Error(w, "Unknown request", http.StatusInternalServerError)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := fn(body)
if err != nil {
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
if this.staticPath != "" {
f, err := os.Open(this.staticPath)
if err != nil {
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() {
log.Fatal(srv.ListenAndServe())
}()
}
const usage = `Usage:
redoctober -static <path> -vaultpath <path> -addr <addr> -certs <path1>[,<path2>,...] -keys <path1>[,<path2>,...] [-ca <path>]
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
)
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()
var certsPath, keysPath string
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", "localhost:8080", "Server and port separated by :")
flag.StringVar(&cli.Server.CAPath, "ca", "", "Path of TLS CA for client authentication (optional)")
flag.StringVar(&certsPath, "certs", "", "Path(s) of TLS certificate in PEM format, comma-separated")
flag.StringVar(&cli.HipChat.Host, "hchost", "", "Hipchat Url Base (ex: hipchat.com)")
flag.StringVar(&cli.HipChat.APIKey, "hckey", "", "Hipchat API Key")
flag.StringVar(&cli.HipChat.Room, "hcroom", "", "Hipchat Room Id")
flag.StringVar(&keysPath, "keys", "", "Path(s) of TLS private key in PEM format, comma-separated, must me in the same order as the certs")
flag.StringVar(&cli.Metrics.Host, "metrics-host", "localhost", "The `host` the metrics endpoint should listen on.")
flag.StringVar(&cli.Metrics.Port, "metrics-port", "8081", "The `port` the metrics endpoint should listen on.")
flag.StringVar(&cli.UI.Root, "rohost", "", "RedOctober Url Base (ex: localhost:8080)")
flag.StringVar(&cli.UI.Static, "static", "", "Path to override built-in index.html")
flag.BoolVar(&cli.Server.Systemd, "systemdfds", false, "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()
cli.Server.CertPaths = strings.Split(certsPath, ",")
cli.Server.KeyPaths = strings.Split(keysPath, ",")
}
//go:generate go run generate.go
func main() {
var err error
if confFile != "" {
cfg, err = config.Load(confFile)
if err != nil {
log.Fatal(err)
}
}
cfg.Merge(cli)
if vaultPath == "" || !cfg.Valid() {
fmt.Fprint(os.Stderr, usage)
flag.PrintDefaults()
os.Exit(2)
}
if err := core.Init(vaultPath, cfg); err != nil {
log.Fatal(err)
}
initPrometheus()
s, l, err := NewServer(cfg.UI.Static, cfg.Server.Addr, cfg.Server.CAPath,
cfg.Server.CertPaths, cfg.Server.KeyPaths, cfg.Server.Systemd)
if err != nil {
log.Fatalf("Error starting redoctober server: %s\n", err)
}
log.Printf("http.serve start: addr=%s", cfg.Server.Addr)
log.Fatal(s.Serve(l))
}