267 lines
8 KiB
Go
267 lines
8 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// Frontend exposes statuses over HTTP(S)
|
|
type Frontend struct {
|
|
backend Backend
|
|
host string
|
|
port int
|
|
certfile string
|
|
keyfile string
|
|
tlsMinVersion string
|
|
tlsCiphers []string
|
|
}
|
|
|
|
var backend Backend
|
|
var logFormat string
|
|
|
|
// Start creates an HTTP server and listen
|
|
func (f *Frontend) Start() error {
|
|
Debug("creating router")
|
|
r := mux.NewRouter()
|
|
r.Use(loggingMiddleware)
|
|
r.Use(headersMiddleware)
|
|
|
|
Debug("registering routes")
|
|
r.HandleFunc("/health", HealthHandler).Methods("GET")
|
|
r.HandleFunc("/master", PrimaryHandler).Methods("GET", "OPTIONS")
|
|
r.HandleFunc("/primary", PrimaryHandler).Methods("GET", "OPTIONS")
|
|
r.HandleFunc("/read-write", ReadWriteHandler).Methods("GET", "OPTIONS")
|
|
r.HandleFunc("/replica", ReplicaHandler).Methods("GET", "OPTIONS")
|
|
r.HandleFunc("/read-only", ReadOnlyHandler).Methods("GET", "OPTIONS")
|
|
|
|
Info("listening on %s", f)
|
|
var err error
|
|
server := &http.Server{
|
|
Addr: f.String(),
|
|
Handler: r,
|
|
}
|
|
if f.certfile != "" && f.keyfile != "" {
|
|
config := &tls.Config{}
|
|
if f.tlsMinVersion != "" {
|
|
config.MinVersion, err = parseTLSVersion(f.tlsMinVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(f.tlsCiphers) > 0 {
|
|
ciphers, err := parseCiphersSuite(f.tlsCiphers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.CipherSuites = ciphers
|
|
}
|
|
|
|
server.TLSConfig = config
|
|
err = server.ListenAndServeTLS(f.certfile, f.keyfile)
|
|
} else {
|
|
err = server.ListenAndServe()
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *Frontend) String() string {
|
|
return fmt.Sprintf("%s:%d", f.host, f.port)
|
|
}
|
|
|
|
// NewFrontend creates a Frontend
|
|
func NewFrontend(config FrontendConfig, b Backend) (*Frontend, error) {
|
|
backend = b
|
|
logFormat = config.LogFormat
|
|
return &Frontend{
|
|
host: config.Host,
|
|
port: config.Port,
|
|
certfile: config.Certfile,
|
|
keyfile: config.Keyfile,
|
|
tlsMinVersion: config.TLSMinVersion,
|
|
tlsCiphers: config.TLSCiphers,
|
|
}, nil
|
|
}
|
|
|
|
// Log requests
|
|
func loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
Info(formatRequest(r, logFormat))
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// formatRequest replaces request placeholders for logging purpose
|
|
func formatRequest(r *http.Request, format string) string {
|
|
if format == "" {
|
|
format = "%a - %m %U"
|
|
}
|
|
definitions := map[string]string{
|
|
"%a": r.RemoteAddr,
|
|
"%m": r.Method,
|
|
"%U": r.RequestURI,
|
|
}
|
|
output := format
|
|
|
|
for placeholder, value := range definitions {
|
|
output = strings.Replace(output, placeholder, value, -1)
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
// Add headers
|
|
func headersMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// HealthHandler returns frontend health status
|
|
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
io.WriteString(w, `{"healthy": true}`)
|
|
}
|
|
|
|
// PrimaryHandler exposes primary status
|
|
func PrimaryHandler(w http.ResponseWriter, r *http.Request) {
|
|
var message string
|
|
var status int
|
|
primary, err := backend.IsPrimary()
|
|
if err != nil {
|
|
message = fmt.Sprintf("{\"error\":\"%v\"}", err)
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
message = fmt.Sprintf("{\"primary\":%t}", primary)
|
|
status = http.StatusServiceUnavailable
|
|
if primary {
|
|
status = http.StatusOK
|
|
}
|
|
w.WriteHeader(status)
|
|
io.WriteString(w, message)
|
|
}
|
|
|
|
// ReadWriteHandler exposes read-write status
|
|
func ReadWriteHandler(w http.ResponseWriter, r *http.Request) {
|
|
var message string
|
|
var status int
|
|
readWrite, err := backend.IsReadWrite()
|
|
if err != nil {
|
|
message = fmt.Sprintf("{\"error\":\"%v\"}", err)
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
message = fmt.Sprintf("{\"read-write\":%t}", readWrite)
|
|
status = http.StatusServiceUnavailable
|
|
if readWrite {
|
|
status = http.StatusOK
|
|
}
|
|
w.WriteHeader(status)
|
|
io.WriteString(w, message)
|
|
}
|
|
|
|
// ReplicaHandler exposes replica status
|
|
func ReplicaHandler(w http.ResponseWriter, r *http.Request) {
|
|
var message string
|
|
var status int
|
|
replica, err := backend.IsReplica()
|
|
if err != nil {
|
|
message = fmt.Sprintf("{\"error\":\"%v\"}", err)
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
message = fmt.Sprintf("{\"replica\":%t}", replica)
|
|
status = http.StatusServiceUnavailable
|
|
if replica {
|
|
status = http.StatusOK
|
|
}
|
|
w.WriteHeader(status)
|
|
io.WriteString(w, message)
|
|
}
|
|
|
|
// ReadOnlyHandler exposes read-only status
|
|
func ReadOnlyHandler(w http.ResponseWriter, r *http.Request) {
|
|
var message string
|
|
var status int
|
|
readOnly, err := backend.IsReadOnly()
|
|
if err != nil {
|
|
message = fmt.Sprintf("{\"error\":\"%v\"}", err)
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
message = fmt.Sprintf("{\"read-only\":%t}", readOnly)
|
|
status = http.StatusServiceUnavailable
|
|
if readOnly {
|
|
status = http.StatusOK
|
|
}
|
|
w.WriteHeader(status)
|
|
io.WriteString(w, message)
|
|
}
|
|
|
|
// Store TLS ciphers map from string to constant
|
|
// See full list at https://golang.org/pkg/crypto/tls/#pkg-constants
|
|
var tlsCiphers = map[string]uint16{
|
|
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
|
|
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
|
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
|
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
|
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
|
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
|
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
|
|
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
|
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
|
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
"TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
|
|
"TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
|
|
"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
|
|
"TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
|
|
}
|
|
|
|
// Convert a list of ciphers from string to TLS constants
|
|
func parseCiphersSuite(strings []string) (ciphers []uint16, err error) {
|
|
for _, s := range strings {
|
|
if cipher, ok := tlsCiphers[s]; ok {
|
|
ciphers = append(ciphers, cipher)
|
|
} else {
|
|
return nil, fmt.Errorf("unknown cipher detected: %s", s)
|
|
}
|
|
}
|
|
return ciphers, nil
|
|
}
|
|
|
|
// Store TLS versions map from string to constant
|
|
// See full list at https://golang.org/pkg/crypto/tls/#pkg-constants
|
|
var tlsVersions = map[string]uint16{
|
|
"SSLv3.0": tls.VersionSSL30,
|
|
"TLSv1.0": tls.VersionTLS10,
|
|
"TLSv1.1": tls.VersionTLS11,
|
|
"TLSv1.2": tls.VersionTLS12,
|
|
"TLSv1.3": tls.VersionTLS13,
|
|
}
|
|
|
|
// Convert a list of ciphers from string to TLS constants
|
|
func parseTLSVersion(s string) (uint16, error) {
|
|
if version, ok := tlsVersions[s]; ok {
|
|
return version, nil
|
|
}
|
|
return 0, fmt.Errorf("unknown TLS version: %s", s)
|
|
}
|