diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/http.go b/vendor/git.autistici.org/ai3/go-common/serverutil/http.go new file mode 100644 index 0000000000000000000000000000000000000000..b1d4b94968edc27e116836d981440d24ebeae60e --- /dev/null +++ b/vendor/git.autistici.org/ai3/go-common/serverutil/http.go @@ -0,0 +1,118 @@ +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) +} diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/json.go b/vendor/git.autistici.org/ai3/go-common/serverutil/json.go new file mode 100644 index 0000000000000000000000000000000000000000..b307932eb878197d49c945e51e5f42adb240747e --- /dev/null +++ b/vendor/git.autistici.org/ai3/go-common/serverutil/json.go @@ -0,0 +1,37 @@ +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) +} diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/load_shedding.go b/vendor/git.autistici.org/ai3/go-common/serverutil/load_shedding.go new file mode 100644 index 0000000000000000000000000000000000000000..beb2ae00d04b988e000392a2931f951d4772dfc8 --- /dev/null +++ b/vendor/git.autistici.org/ai3/go-common/serverutil/load_shedding.go @@ -0,0 +1,51 @@ +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) +} diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/tls.go b/vendor/git.autistici.org/ai3/go-common/serverutil/tls.go new file mode 100644 index 0000000000000000000000000000000000000000..926488f4c6e566a828021c775faff3529c8bc168 --- /dev/null +++ b/vendor/git.autistici.org/ai3/go-common/serverutil/tls.go @@ -0,0 +1,119 @@ +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 +} diff --git a/vendor/vendor.json b/vendor/vendor.json index be247eafb0c1f311d181daa8f1d47e6477cc98e7..248c6cb957d05515cdf8df0ec071d3a4c7218557 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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",