Capstan is a Golang web framework that shares some similarities with others in its segment.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

372 lines
10 KiB

package capstan
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"strings"
"time"
"git.destrealm.org/go/capstan/internal/sys"
)
// TODO: Consider refactoring named listeners to store their network address
// instead.
// Apply http.Server instances to all stored listeners. Errors will be returned
// via the error channel errc.
//
// This increments the Router waitgroup once for each configured listener.
func (r *Router) listen(errc chan error) {
for _, listener := range r.listeners {
r.AttachListener(listener)
}
}
// Setup listeners to all configured addresses. Currently this only sets up 3
// listeners for each of the following, if configured: ListenAddress,
// ListenAddressTLS, and ListenSocket.
//
// If you wish to attach custom listeners, you must call AddListener (below) and
// it should implement both net.Listener and syscall.Conn. While simply
// implementing net.Listener should be sufficient, it may fail for high
// availability configurations that expect graceful restart capabilities.
func (r *Router) setupListeners() error {
var requiresPriv bool
config := r.app.Config().Server
r.proxy.Switch(r.mux)
if len(r.listeners) == 0 {
requiresPriv = isPrivPort(r.app.Config().Server.ListenAddress)
if listener, err := r.ListenHTTP(r.app.Config().Server.ListenAddress); err != nil {
return err
} else {
r.namedListeners.tcp = listener
r.AddListener(listener)
}
requiresPriv = isPrivPort(r.app.Config().Server.ListenAddressTLS)
if listener, err := r.ListenTLS(r.app.Config().Server.ListenAddressTLS); err != nil {
return err
} else {
r.namedListeners.tls = listener
r.AddListener(listener)
}
if listener, err := r.ListenSocket(r.app.Config().Server.ListenSocket); err != nil {
return err
} else {
r.namedListeners.sock = listener
r.AddListener(listener)
}
}
if requiresPriv && sys.HasSuperuser() {
if config.RunAs == "" {
return fmt.Errorf("application is running as superuser with no RunAs configured")
}
err := sys.SwitchUser(config.RunAs)
if err != nil {
return fmt.Errorf("unable to drop privileges: %v", err)
}
}
return nil
}
// AddListener accepts a listener against which the Chi handler will be attached
// and will be used as the base listener for an HTTP or HTTPS service.
//
// This listener will be mapped to the configured network management as defined
// in network.go and netlisten_*.go. If the specified listener does not
// implement both net.Listen and syscall.Conn it will not be used for high
// availability graceful restart. If you depend on a custom listener, you may
// need to implement graceful restart yourself or implement syscall.Conn.
func (r *Router) AddListener(listener net.Listener) {
if listener != nil {
r.listeners = append(r.listeners, listener)
}
}
// AttachListener to the current route handler.
//
// This may be used by external code to enable listeners.
func (r *Router) AttachListener(listener net.Listener) {
var server HTTPServer
server.Handler = r.proxy
server.ReadHeaderTimeout = time.Second * time.Duration(r.app.Config().Server.HeaderTimeout)
server.IdleTimeout = time.Second * time.Duration(r.app.Config().Server.IdleConnectionTimeout)
server.MaxHeaderBytes = r.app.Config().Server.MaxHeaderLength
r.servers = append(r.servers, &server)
r.wg.Add(1)
go func(l net.Listener) {
if err := server.Serve(l); err != http.ErrServerClosed {
r.Close()
}
r.wg.Done()
}(listener)
}
func (r *Router) AttachNamedListener(name string, listener net.Listener) {
switch name {
case "tls":
r.namedListeners.tls = listener
case "tcp":
r.namedListeners.tcp = listener
case "sock":
fallthrough
case "socket":
fallthrough
case "file":
r.namedListeners.sock = listener
}
r.AttachListener(listener)
}
// Close all configured listeners gracefully. This calls Shutdown on each
// http.Server and waits until all connections have closed before terminating.
func (r *Router) Close() {
r.app.Logger().Noticef("Shutting down (waiting max %d second(s) for connections to close)...", r.app.Config().Server.ShutdownTimeout)
for _, server := range r.servers {
ctx, cancel := context.WithTimeout(r.shutdownCtx,
time.Second*time.Duration(r.app.Config().Server.ShutdownTimeout))
defer cancel()
r.app.Logger().Debugf("closing listener: (%s) %s", server.Listener().Addr().Network(), server.Listener().Addr().String())
err := server.Shutdown(ctx)
if err != nil {
r.app.Logger().Warning("Error shutting down server:", err)
}
}
for _, listener := range r.listeners {
listener.Close()
}
}
// CloseNamed listener.
//
// This will gracefully shut down the named listener, if available. The special
// names "tls," "tcp," and "file" are used to close reserved listeners for TLS,
// TCP, and Unix domain socket traffic.
//
// This will also remove the listener from the router's listeners store.
func (r *Router) CloseNamed(name string) (err error) {
var listener net.Listener
switch name {
case "tls":
listener = r.namedListeners.tls
case "tcp":
listener = r.namedListeners.tcp
case "sock":
fallthrough
case "socket":
fallthrough
case "file":
listener = r.namedListeners.sock
}
if listener == nil {
return
}
l := make([]net.Listener, 0)
for _, mapped := range r.listeners {
if mapped != listener {
l = append(l, mapped)
}
}
r.listeners = l
r.app.Logger().Error("Shutting down listener (waiting max 10 seconds for connections to close)...")
ctx, _ := context.WithTimeout(r.shutdownCtx, time.Second*10)
for _, server := range r.servers {
if server.Listener() == listener {
if err = server.Shutdown(ctx); err != nil {
r.app.Logger().Warning("error shutting down listener:", err)
return
} else {
StopListening(listener)
}
}
}
switch name {
case "tls":
r.namedListeners.tls = nil
case "tcp":
r.namedListeners.tcp = nil
case "sock":
fallthrough
case "socket":
fallthrough
case "file":
r.namedListeners.sock = nil
}
return
}
// Listen on all configured listeners and wait until they close.
//
// If an error is received by this function it will call Router.Close itself and
// begin the shutdown process.
func (r *Router) Listen() error {
var err error
errc := make(chan error)
if err := r.setupListeners(); err != nil {
return err
}
r.listen(errc)
go func() {
err = <-errc
if err != nil {
r.Close()
r.wg.Done()
return
}
}()
r.wg.Wait()
return err
}
// ListenHTTP allows client code to configure a new port to listen on. This
// should not be called if the server is already listening on an HTTP port. This
// is called internally if ListenAddress is set.
func (r *Router) ListenHTTP(addr string) (net.Listener, error) {
// An HTTP port most liekly wasn't specified.
if addr == "" {
return nil, nil
}
return Listen("tcp", addr)
}
// ListenSocket allows client code to configure a new UNIX domain socket to
// listen on. This should not be called if the server is already listening on
// the same socket. This is called internally if ListenSocket is set.
func (r *Router) ListenSocket(sock string) (net.Listener, error) {
if sock == "" {
return nil, nil
}
return Listen("unix", sock)
}
// ListenTLS allows client code to configure a new TLS port to listen on. This
// should not be called if the server is already listening on a TLS port. This
// is called internally if ListenAddressTLS is set.
func (r *Router) ListenTLS(addr string) (net.Listener, error) {
// TLS most likely wasn't specified.
if addr == "" {
return nil, nil
}
var err error
var tc *tls.Config
var pool *x509.CertPool
ciphers := []uint16{
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
// No forward secrecy but disabling these may reduce client
// compatibility.
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
}
config := r.app.Config().Server
if config.TLSConfig != nil {
tc = config.TLSConfig
} else {
host := config.CalculatedHostname()
if strings.Contains(host, ":") {
host, _, err = net.SplitHostPort(host)
if err != nil && host == "" {
host = "localhost"
} else if err != nil {
return nil, err
}
}
if config.MinVersion == 0 {
config.MinVersion = tls.VersionTLS11
}
if config.MaxVersion == 0 {
config.MaxVersion = tls.VersionTLS13
}
pool = x509.NewCertPool()
tc = &tls.Config{
Certificates: config.Certificates,
RootCAs: pool,
ServerName: host,
MinVersion: config.MinVersion,
MaxVersion: config.MaxVersion,
CipherSuites: ciphers,
NextProtos: []string{"h2"},
// https://blog.cloudflare.com/exposing-go-on-the-internet/
PreferServerCipherSuites: true,
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
},
}
}
listener, err := Listen("tcp", addr)
if err != nil {
return nil, err
}
return tls.NewListener(listener, tc), nil
}
// HasSocket returns true if Capstan was configured to listen on a domain
// socket.
//
// As with other Has* functions for listeners, this does not indicate a status
// for manually-configured listeners and applies only to those that were started
// via the Listen* methods (either automatically, via configuration, or
// manually).
func (r *Router) HasSocket() bool {
return r.namedListeners.sock != nil
}
// HasTCP returns true if Capstan was configured to listen on a TCP socket.
//
// As with other Has* functions for listeners, this does not indicate a status
// for manually-configured listeners and applies only to those that were started
// via the Listen* methods (either automatically, via configuration, or
// manually).
func (r *Router) HasTCP() bool {
return r.namedListeners.tcp != nil
}
// HasTLS returns true if Capstan was configured to listen on a TLS socket.
//
// As with other Has* functions for listeners, this does not indicate a status
// for manually-configured listeners and applies only to those that were started
// via the Listen* methods (either automatically, via configuration, or
// manually).
func (r *Router) HasTLS() bool {
return r.namedListeners.tls != nil
}