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