Files
upstream-mirror b352a9a30d
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s
snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:17:21 +05:00

185 lines
9.2 KiB
Go

package configtypes
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
// TLSConfig is a common configuration for TLS.
type TLSConfig struct {
// Enabled turns on using TLS.
Enabled bool `mapstructure:"enabled" json:"enabled" yaml:"enabled" toml:"enabled" envconfig:"enabled" doc:"Enables TLS for this entity."`
// CertPem is a PEM certificate.
CertPem PEMData `mapstructure:"cert_pem" json:"cert_pem" envconfig:"cert_pem" yaml:"cert_pem" toml:"cert_pem" expose:"full" doc:"Certificate in PEM format. May be provided as a file path, raw PEM string, or base64-encoded PEM."`
// KeyPem is a path to a file with key in PEM format.
KeyPem PEMData `mapstructure:"key_pem" json:"key_pem" envconfig:"key_pem" yaml:"key_pem" toml:"key_pem" doc:"Private key in PEM format matching the certificate. May be provided as a file path, raw PEM string, or base64-encoded PEM."`
// ServerCAPem is a server root CA certificate in PEM format.
// The client uses this certificate to verify the server's certificate during the TLS handshake.
ServerCAPem PEMData `mapstructure:"server_ca_pem" json:"server_ca_pem" envconfig:"server_ca_pem" yaml:"server_ca_pem" toml:"server_ca_pem" expose:"full" doc:"Root CA certificate in PEM format used to verify the server's certificate during the handshake (client side). May be a file path, raw PEM, or base64-encoded PEM."`
// ClientCAPem is a client CA certificate in PEM format.
// The server uses this certificate to verify the client's certificate during the TLS handshake.
ClientCAPem PEMData `mapstructure:"client_ca_pem" json:"client_ca_pem" envconfig:"client_ca_pem" yaml:"client_ca_pem" toml:"client_ca_pem" expose:"full" doc:"CA certificate in PEM format used to verify client certificates (server side). Setting it enables mutual TLS and requires clients to present a valid certificate. May be a file path, raw PEM, or base64-encoded PEM."`
// InsecureSkipVerify turns off server certificate verification.
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify" json:"insecure_skip_verify" envconfig:"insecure_skip_verify" yaml:"insecure_skip_verify" toml:"insecure_skip_verify" doc:"Disables verification of the server's certificate chain and host name. Insecure - use only for testing."`
// ServerName is used to verify the hostname on the returned certificates.
ServerName string `mapstructure:"server_name" json:"server_name" envconfig:"server_name" yaml:"server_name" toml:"server_name" expose:"full" doc:"Expected server name (SNI) used to verify the hostname on the returned certificate."`
}
func (c TLSConfig) ToGoTLSConfig(logTraceEntity string) (*tls.Config, error) {
if !c.Enabled {
return nil, nil
}
logger := log.With().Str("entity", logTraceEntity).Logger()
logger.Debug().Msg("TLS enabled")
tlsConfig, err := makeTLSConfig(c, logger, os.ReadFile, os.Stat)
if err != nil {
return nil, fmt.Errorf("error make TLS config (for %s): %w", logTraceEntity, err)
}
return tlsConfig, nil
}
// ReadFileFunc is like os.ReadFile but helps in testing.
type ReadFileFunc func(name string) ([]byte, error)
// StatFileFunc is like os.Stat but helps in testing.
type StatFileFunc func(name string) (os.FileInfo, error)
// makeTLSConfig constructs a tls.Config instance using the given configuration.
func makeTLSConfig(cfg TLSConfig, logger zerolog.Logger, readFile ReadFileFunc, statFile StatFileFunc) (*tls.Config, error) {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
if err := loadCertificate(cfg, logger, tlsConfig, readFile, statFile); err != nil {
return nil, fmt.Errorf("error load certificate: %w", err)
}
if err := loadServerCA(cfg, logger, tlsConfig, readFile, statFile); err != nil {
return nil, fmt.Errorf("error load server CA: %w", err)
}
if err := loadClientCA(cfg, logger, tlsConfig, readFile, statFile); err != nil {
return nil, fmt.Errorf("error load client CA: %w", err)
}
tlsConfig.ServerName = cfg.ServerName
tlsConfig.InsecureSkipVerify = cfg.InsecureSkipVerify
logger.Debug().Str("server_name", cfg.ServerName).Bool("insecure_skip_verify", cfg.InsecureSkipVerify).Msg("TLS config options set")
logger.Debug().Msg("TLS config created")
return tlsConfig, nil
}
// loadCertificate loads the TLS certificate from various sources.
func loadCertificate(cfg TLSConfig, logger zerolog.Logger, tlsConfig *tls.Config, readFile ReadFileFunc, statFile StatFileFunc) error {
var certPEMBlock, keyPEMBlock []byte
var err error
switch {
case cfg.CertPem != "" && cfg.KeyPem != "":
var pemSource string
certPEMBlock, pemSource, err = cfg.CertPem.Load(statFile, readFile)
if err != nil {
return fmt.Errorf("load TLS certificate: %w", err)
}
logger.Debug().Str("pem_source", pemSource).Msg("loaded PEM certificate")
keyPEMBlock, pemSource, err = cfg.KeyPem.Load(statFile, readFile)
if err != nil {
return fmt.Errorf("load TLS key: %w", err)
}
logger.Debug().Str("pem_source", pemSource).Msg("loaded PEM key")
default:
}
if len(certPEMBlock) > 0 && len(keyPEMBlock) > 0 {
logger.Debug().Msg("create x509 key pair")
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return fmt.Errorf("error create x509 key pair: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
} else {
logger.Debug().Msg("no cert or key provided, skip loading x509 key pair")
}
return nil
}
// loadServerCA loads the root CA from various sources.
func loadServerCA(cfg TLSConfig, logger zerolog.Logger, tlsConfig *tls.Config, readFile ReadFileFunc, statFile StatFileFunc) error {
if cfg.ServerCAPem == "" {
logger.Debug().Msg("no server CA certificate provided")
return nil
}
caCert, err := loadPEMData(cfg.ServerCAPem, logger, "server CA", readFile, statFile)
if err != nil {
return fmt.Errorf("error load server CA certificate: %w", err)
}
if len(caCert) > 0 {
logger.Debug().Msg("load server CA certificate")
caCertPool, err := newCertPoolFromPEM(caCert)
if err != nil {
return fmt.Errorf("error create server CA certificate pool: %w", err)
}
tlsConfig.RootCAs = caCertPool
} else {
logger.Debug().Msg("no server CA certificate provided")
}
return nil
}
// loadClientCA loads the client CA from various sources.
func loadClientCA(cfg TLSConfig, logger zerolog.Logger, tlsConfig *tls.Config, readFile ReadFileFunc, statFile StatFileFunc) error {
if cfg.ClientCAPem == "" {
logger.Debug().Msg("no client CA certificate provided")
return nil
}
caCert, err := loadPEMData(cfg.ClientCAPem, logger, "client CA", readFile, statFile)
if err != nil {
return err
}
if len(caCert) > 0 {
logger.Debug().Msg("load client CA certificate")
caCertPool, err := newCertPoolFromPEM(caCert)
if err != nil {
return fmt.Errorf("error create client CA certificate pool: %w", err)
}
tlsConfig.ClientCAs = caCertPool
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
} else {
logger.Debug().Msg("no client CA certificate provided")
}
return nil
}
// loadPEMData attempts to load PEM data from a file, base64 string, or raw string.
func loadPEMData(pemData PEMData, logger zerolog.Logger, certType string, readFile ReadFileFunc, statFile StatFileFunc) ([]byte, error) {
pemBytes, pemSource, err := pemData.Load(statFile, readFile)
if err != nil {
return nil, fmt.Errorf("load PEM block for %s: %w", certType, err)
}
logger.Debug().Str("pem_source", pemSource).Msg("loaded PEM data")
return pemBytes, nil
}
// newCertPoolFromPEM returns certificate pool for the given PEM-encoded
// certificate bundle. Note that it currently ignores invalid blocks.
func newCertPoolFromPEM(pem []byte) (*x509.CertPool, error) {
certPool := x509.NewCertPool()
ok := certPool.AppendCertsFromPEM(pem)
if !ok {
return nil, errors.New("no valid certificates found")
}
return certPool, nil
}
type TLSAutocert struct {
Enabled bool `mapstructure:"enabled" json:"enabled" envconfig:"enabled" yaml:"enabled" toml:"enabled" doc:"Enables automatic TLS certificate provisioning via ACME (Let's Encrypt)."`
HostWhitelist []string `mapstructure:"host_whitelist" json:"host_whitelist" envconfig:"host_whitelist" yaml:"host_whitelist" toml:"host_whitelist" expose:"full" doc:"List of host names ACME certificates are allowed to be issued for. Leave empty to allow any host (not recommended in production)."`
CacheDir string `mapstructure:"cache_dir" json:"cache_dir" envconfig:"cache_dir" yaml:"cache_dir" toml:"cache_dir" expose:"full" doc:"Directory to cache issued certificates so they survive restarts and are shared between nodes."`
Email string `mapstructure:"email" json:"email" envconfig:"email" yaml:"email" toml:"email" expose:"full" doc:"Contact email sent to the ACME provider for expiry notices and account registration."`
ServerName string `mapstructure:"server_name" json:"server_name" envconfig:"server_name" yaml:"server_name" toml:"server_name" expose:"full" doc:"Server name (SNI) for which the certificate is requested."`
HTTP bool `mapstructure:"http" json:"http" envconfig:"http" yaml:"http" toml:"http" doc:"Enables an HTTP server to handle ACME http-01 challenges instead of the default tls-alpn-01."`
HTTPAddr string `mapstructure:"http_addr" json:"http_addr" envconfig:"http_addr" default:":80" yaml:"http_addr" toml:"http_addr" expose:"full" doc:"Address to bind the ACME http-01 challenge server to. Default <<:80>>."`
}