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)
 | |
| }
 |