Commit ff465de0 authored by ale's avatar ale

Add ai3/go-common vendor dep

parent 9d388e96
Pipeline #655 passed with stages
in 1 minute and 7 seconds
package serverutil
import (
"context"
"crypto/tls"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var gracefulShutdownTimeout = 3 * time.Second
// ServerConfig stores common HTTP/HTTPS server configuration parameters.
type ServerConfig struct {
TLS *TLSServerConfig `yaml:"tls"`
MaxInflightRequests int `yaml:"max_inflight_requests"`
}
// Serve HTTP(S) content on the specified address. If serverConfig is
// not nil, enable HTTPS and TLS authentication.
//
// This function will return an error if there are problems creating
// the listener, otherwise it will handle graceful termination on
// SIGINT or SIGTERM and return nil.
func Serve(h http.Handler, serverConfig *ServerConfig, addr string) (err error) {
var tlsConfig *tls.Config
if serverConfig != nil {
if serverConfig.TLS != nil {
tlsConfig, err = serverConfig.TLS.TLSConfig()
if err != nil {
return err
}
h, err = serverConfig.TLS.TLSAuthWrapper(h)
if err != nil {
return err
}
}
if serverConfig.MaxInflightRequests > 0 {
h = newLoadSheddingWrapper(serverConfig.MaxInflightRequests, h)
}
}
// These are not meant to be external-facing servers, so we
// can be generous with the timeouts to keep the number of
// reconnections low.
srv := &http.Server{
Addr: addr,
Handler: instrumentHandler(h),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 600 * time.Second,
TLSConfig: tlsConfig,
}
// Install a signal handler for gentle process termination.
done := make(chan struct{})
sigCh := make(chan os.Signal, 1)
go func() {
<-sigCh
log.Printf("exiting")
// Gracefully terminate for 3 seconds max, then shut
// down remaining clients.
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
defer cancel()
if err := srv.Shutdown(ctx); err == context.Canceled {
if err := srv.Close(); err != nil {
log.Printf("error terminating server: %v", err)
}
}
close(done)
}()
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
return err
}
<-done
return nil
}
func instrumentHandler(h http.Handler) http.Handler {
root := http.NewServeMux()
root.Handle("/metrics", promhttp.Handler())
root.Handle("/", h)
return promhttp.InstrumentHandlerInFlight(inFlightRequests,
promhttp.InstrumentHandlerCounter(totalRequests, root))
}
// HTTP-related metrics.
var (
totalRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "total_requests",
Help: "Total number of requests.",
},
[]string{"code"},
)
inFlightRequests = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "inflight_requests",
Help: "Number of in-flight requests.",
},
)
)
func init() {
prometheus.MustRegister(totalRequests, inFlightRequests)
}
package serverutil
import (
"encoding/json"
"net/http"
)
// DecodeJSONRequest decodes a JSON object from an incoming HTTP POST
// request and return true when successful. In case of errors, it will
// write an error response to w and return false.
func DecodeJSONRequest(w http.ResponseWriter, r *http.Request, obj interface{}) bool {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return false
}
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "Need JSON request", http.StatusBadRequest)
return false
}
if err := json.NewDecoder(r.Body).Decode(obj); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return false
}
return true
}
// EncodeJSONResponse writes an application/json response to w.
func EncodeJSONResponse(w http.ResponseWriter, obj interface{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Expires", "-1")
w.Header().Set("X-Content-Type-Options", "nosniff")
_ = json.NewEncoder(w).Encode(obj)
}
package serverutil
import (
"net/http"
"sync/atomic"
"github.com/prometheus/client_golang/prometheus"
)
type loadSheddingWrapper struct {
limit, inflight int32
h http.Handler
}
func newLoadSheddingWrapper(limit int, h http.Handler) *loadSheddingWrapper {
return &loadSheddingWrapper{limit: int32(limit), h: h}
}
func (l *loadSheddingWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
inflight := atomic.AddInt32(&l.inflight, 1)
defer atomic.AddInt32(&l.inflight, -1)
if inflight > l.limit {
throttledRequests.Inc()
w.Header().Set("Connection", "close")
http.Error(w, "Throttled", http.StatusTooManyRequests)
return
}
allowedRequests.Inc()
l.h.ServeHTTP(w, r)
}
var (
throttledRequests = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "ls_throttled_requests",
Help: "Requests throttled by the load shedding wrapper.",
},
)
allowedRequests = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "ls_allowed_requests",
Help: "Requests allowed by the load shedding wrapper.",
},
)
)
func init() {
prometheus.MustRegister(throttledRequests, allowedRequests)
}
package serverutil
import (
"crypto/tls"
"net/http"
"regexp"
common "git.autistici.org/ai3/go-common"
)
// TLSAuthACL describes a single access control entry. Path and
// CommonName are anchored regular expressions (they must match the
// entire string).
type TLSAuthACL struct {
Path string `yaml:"path"`
CommonName string `yaml:"cn"`
pathRx, cnRx *regexp.Regexp
}
func (p *TLSAuthACL) compile() error {
var err error
p.pathRx, err = regexp.Compile("^" + p.Path + "$")
if err != nil {
return err
}
p.cnRx, err = regexp.Compile("^" + p.CommonName + "$")
return err
}
func (p *TLSAuthACL) match(req *http.Request) bool {
if !p.pathRx.MatchString(req.URL.Path) {
return false
}
for _, cert := range req.TLS.PeerCertificates {
if p.cnRx.MatchString(cert.Subject.CommonName) {
return true
}
}
return false
}
// TLSAuthConfig stores access control lists for TLS authentication. Access
// control lists are matched against the request path and the
// CommonName component of the peer certificate subject.
type TLSAuthConfig struct {
Allow []*TLSAuthACL `yaml:"allow"`
}
func (c *TLSAuthConfig) match(req *http.Request) bool {
// Fail *OPEN* if unconfigured.
if c == nil || len(c.Allow) == 0 {
return true
}
for _, acl := range c.Allow {
if acl.match(req) {
return true
}
}
return false
}
// TLSServerConfig configures a TLS server with client authentication
// and authorization based on the client X509 certificate.
type TLSServerConfig struct {
Cert string `yaml:"cert"`
Key string `yaml:"key"`
CA string `yaml:"ca"`
Auth *TLSAuthConfig `yaml:"acl"`
}
// TLSConfig returns a tls.Config created with the current configuration.
func (c *TLSServerConfig) TLSConfig() (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(c.Cert, c.Key)
if err != nil {
return nil, err
}
cas, err := common.LoadCA(c.CA)
if err != nil {
return nil, err
}
// Set some TLS-level parameters (cipher-related), assuming
// we're using EC keys.
tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: cas,
CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384},
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
}
tlsConf.BuildNameToCertificate()
return tlsConf, nil
}
// TLSAuthWrapper protects a root HTTP handler with TLS authentication.
func (c *TLSServerConfig) TLSAuthWrapper(h http.Handler) (http.Handler, error) {
// Compile regexps.
if c.Auth != nil {
for _, acl := range c.Auth.Allow {
if err := acl.compile(); err != nil {
return nil, err
}
}
}
// Build the wrapper function to check client certificates
// identities (looking at the CN part of the X509 subject).
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c.Auth.match(r) {
h.ServeHTTP(w, r)
return
}
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}), nil
}
......@@ -14,6 +14,12 @@
"revision": "245211ebe9f881b575461958274bada3b8e20b7b",
"revisionTime": "2017-11-23T18:34:19Z"
},
{
"checksumSHA1": "3bComZxAfgnoTG4UDlyFgLyeykc=",
"path": "git.autistici.org/ai3/go-common/serverutil",
"revision": "96dc550223598dd5d984bb5fc222323ef239bed7",
"revisionTime": "2017-12-09T10:27:16Z"
},
{
"checksumSHA1": "hJvRJwSx9aZUKF26o/gOmgUJSsE=",
"path": "git.autistici.org/id/auth",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment