diff --git a/serverutil/http.go b/serverutil/http.go new file mode 100644 index 0000000000000000000000000000000000000000..079b9338e8622ee9e5f9f176c9c4a3f1e3a91ea8 --- /dev/null +++ b/serverutil/http.go @@ -0,0 +1,68 @@ +package serverutil + +import ( + "context" + "crypto/tls" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" +) + +// 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 *TLSServerConfig, addr string) (err error) { + var tlsConfig *tls.Config + if serverConfig != nil { + tlsConfig, err = serverConfig.TLSConfig() + if err != nil { + return err + } + h, err = serverConfig.TLSAuthWrapper(h) + if err != nil { + return err + } + } + + srv := &http.Server{ + Addr: addr, + Handler: h, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + TLSConfig: tlsConfig, + } + + 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(), 3*time.Second) + 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 +} diff --git a/serverutil/tls.go b/serverutil/tls.go new file mode 100644 index 0000000000000000000000000000000000000000..926488f4c6e566a828021c775faff3529c8bc168 --- /dev/null +++ b/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 +}