From d77eb9a4565d01b25aeb24dfd49dcad1bde07f46 Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Wed, 21 Jun 2017 17:43:27 +0100 Subject: [PATCH] move server code to an importable package Commit https://github.com/cloudflare/redoctober/commit/6f8424ad38982c92d64b9c70fb1e2e823aeb9f18 added an public function so we can import redoctober's NewServer function in external test packages to create an RO server without having to actually install the binary in test environments. This used to work until https://github.com/golang/go/commit/0f06d0a051714d14b923b0a9164ab1b3f463aa74, which makes it impossible to import main package in external packages. This change moves `NewServer` and its related code to a non-main package so other packages can still import it in tests or any other places. Signed-off-by: Daniel Dao --- redoctober.go | 212 ++------------------------------------------- server/server.go | 221 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 207 deletions(-) create mode 100644 server/server.go diff --git a/redoctober.go b/redoctober.go index 185accc..1454b90 100644 --- a/redoctober.go +++ b/redoctober.go @@ -1,228 +1,23 @@ -// 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/cloudflare/redoctober/server" "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. @@ -268,6 +63,9 @@ func init() { cli = config.New() cfg = config.New() + // customized the default index html with auto generated content + server.DefaultIndexHtml = indexHtml + cli.Server.Addr = defaultAddr cli.Metrics.Host = defaultMetricsHost cli.Metrics.Port = defaultMetricsPort @@ -340,7 +138,7 @@ func main() { 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, + s, l, err := server.NewServer(cfg.UI.Static, cfg.Server.Addr, cfg.Server.CAPath, cpaths, kpaths, cfg.Server.Systemd) if err != nil { report.Check(err, nil) diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..004e0eb --- /dev/null +++ b/server/server.go @@ -0,0 +1,221 @@ +// Package server contains the server code for Red October. +// +// Copyright (c) 2013 CloudFlare, Inc. +package server + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "time" + + "github.com/cloudflare/redoctober/core" + "github.com/cloudflare/redoctober/report" + "github.com/coreos/go-systemd/activation" +) + +// DefaultIndexHtml can be used to customize the package default index page +// when static path is not specified +var DefaultIndexHtml = "" + +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(DefaultIndexHtml)) + } + + 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) +}