mirror of
https://github.com/cloudflare/redoctober.git
synced 2025-12-23 06:15:45 +00:00
move server code to an importable package
Commit6f8424ad38added 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 until0f06d0a051, 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 <dqminh89@gmail.com>
This commit is contained in:
212
redoctober.go
212
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)
|
||||
|
||||
221
server/server.go
Normal file
221
server/server.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user