snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
21
internal/app/cmd.go
Normal file
21
internal/app/cmd.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Centrifugo() *cobra.Command {
|
||||
var configFile string
|
||||
cmd := &cobra.Command{
|
||||
Use: "",
|
||||
Short: "Centrifugo",
|
||||
Long: "Centrifugo – scalable real-time messaging server in language-agnostic way",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run(cmd, configFile)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&configFile, "config", "c", "config.json", "path to config file")
|
||||
config.DefineFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
326
internal/app/engine.go
Normal file
326
internal/app/engine.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/confighelpers"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/controllers"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/natsbroker"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/pgmapbroker"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/pgstreambroker"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/redisnatsbroker"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func configureEngines(node *centrifuge.Node, cfgContainer *config.Container) error {
|
||||
cfg := cfgContainer.Config()
|
||||
|
||||
var broker centrifuge.Broker
|
||||
var presenceManager centrifuge.PresenceManager
|
||||
|
||||
if !cfg.Broker.Enabled || !cfg.PresenceManager.Enabled {
|
||||
var err error
|
||||
var engineMode string
|
||||
switch cfg.Engine.Type {
|
||||
case "memory":
|
||||
broker, presenceManager, err = createMemoryEngine(node)
|
||||
case "redis":
|
||||
broker, presenceManager, engineMode, err = createRedisEngine(node, cfgContainer)
|
||||
default:
|
||||
return fmt.Errorf("unknown engine type: %s", cfg.Engine.Type)
|
||||
}
|
||||
event := log.Info().Str("engine_type", cfg.Engine.Type)
|
||||
if engineMode != "" {
|
||||
event.Str("engine_mode", engineMode)
|
||||
}
|
||||
event.Msg("initializing engine")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating engine: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Info().Msgf("both broker and presence manager enabled, skip engine initialization")
|
||||
}
|
||||
|
||||
if cfg.Broker.Enabled {
|
||||
var err error
|
||||
var brokerMode string
|
||||
switch cfg.Broker.Type {
|
||||
case "memory":
|
||||
broker, err = createMemoryBroker(node)
|
||||
case "redis":
|
||||
broker, brokerMode, err = createRedisBroker(node, cfgContainer)
|
||||
case "nats":
|
||||
broker, err = NatsBroker(node, cfg)
|
||||
brokerMode = "nats"
|
||||
case "postgres":
|
||||
broker, err = createPostgresStreamBroker(node, cfg.Broker.Postgres)
|
||||
brokerMode = "postgres"
|
||||
case "redisnats":
|
||||
if !cfg.EnableUnreleasedFeatures {
|
||||
return fmt.Errorf("redisnats broker requires enable_unreleased_features on")
|
||||
}
|
||||
log.Warn().Msg("redisnats broker is not released, it may be changed or removed at any point")
|
||||
redisBroker, redisBrokerMode, err := createRedisBroker(node, cfgContainer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating redis broker: %v", err)
|
||||
}
|
||||
brokerMode = redisBrokerMode + "+nats"
|
||||
natsBroker, err := NatsBroker(node, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating nats broker: %v", err)
|
||||
}
|
||||
broker, err = redisnatsbroker.New(natsBroker, redisBroker)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating redisnats broker: %v", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown broker type: %s", cfg.Broker.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating broker: %v", err)
|
||||
}
|
||||
event := log.Info().Str("broker_type", cfg.Broker.Type)
|
||||
if brokerMode != "" {
|
||||
event.Str("broker_mode", brokerMode)
|
||||
}
|
||||
event.Msg("broker is enabled, using it instead of broker from engine")
|
||||
} else {
|
||||
log.Info().Msgf("explicit broker not provided, using the one from engine")
|
||||
}
|
||||
|
||||
if cfg.PresenceManager.Enabled {
|
||||
var err error
|
||||
var presenceManagerMode string
|
||||
switch cfg.PresenceManager.Type {
|
||||
case "memory":
|
||||
presenceManager, err = createMemoryPresenceManager(node)
|
||||
case "redis":
|
||||
presenceManager, presenceManagerMode, err = createRedisPresenceManager(node, cfgContainer)
|
||||
default:
|
||||
return fmt.Errorf("unknown presence manager type: %s", cfg.PresenceManager.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating presence manager: %v", err)
|
||||
}
|
||||
event := log.Info().Str("presence_manager_type", cfg.PresenceManager.Type)
|
||||
if presenceManagerMode != "" {
|
||||
event.Str("presence_manager_mode", presenceManagerMode)
|
||||
}
|
||||
event.Msg("presence manager is enabled, using it instead of presence manager from engine")
|
||||
} else {
|
||||
log.Info().Msgf("explicit presence manager not provided, using the one from engine")
|
||||
}
|
||||
|
||||
if cfg.Controller.Enabled {
|
||||
controller, err := controllers.New(node, cfg.Controller)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating controller: %v", err)
|
||||
}
|
||||
if controller != nil {
|
||||
node.SetController(controller)
|
||||
}
|
||||
}
|
||||
|
||||
node.SetBroker(broker)
|
||||
node.SetPresenceManager(presenceManager)
|
||||
return nil
|
||||
}
|
||||
|
||||
func createMemoryBroker(n *centrifuge.Node) (centrifuge.Broker, error) {
|
||||
brokerConf, err := memoryBrokerConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
broker, err := centrifuge.NewMemoryBroker(n, *brokerConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return broker, nil
|
||||
}
|
||||
|
||||
func createPostgresStreamBroker(node *centrifuge.Node, pgCfg configtypes.PostgresStreamBroker) (centrifuge.Broker, error) {
|
||||
pgBrokerCfg := pgstreambroker.PostgresStreamBrokerConfig{
|
||||
DSN: pgCfg.DSN,
|
||||
TLS: pgCfg.TLS,
|
||||
PoolSize: pgCfg.PoolSize,
|
||||
NumShards: pgCfg.NumShards,
|
||||
CleanupInterval: pgCfg.CleanupInterval.ToDuration(),
|
||||
IdempotentResultTTL: pgCfg.IdempotentResultTTL.ToDuration(),
|
||||
BinaryData: pgCfg.BinaryData,
|
||||
StreamRetention: pgCfg.StreamRetention.ToDuration(),
|
||||
UseNotify: pgCfg.UseNotify,
|
||||
NotifyDSN: pgCfg.NotifyDSN,
|
||||
TablePrefix: pgCfg.TablePrefix,
|
||||
PartitionLookaheadDays: pgCfg.PartitionLookaheadDays,
|
||||
PartitionRetentionDays: pgCfg.PartitionRetentionDays,
|
||||
Outbox: pgstreambroker.OutboxConfig{
|
||||
PollInterval: pgCfg.Outbox.PollInterval.ToDuration(),
|
||||
BatchSize: pgCfg.Outbox.BatchSize,
|
||||
},
|
||||
}
|
||||
broker, err := pgstreambroker.NewPostgresStreamBroker(node, pgBrokerCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating Postgres stream broker: %w", err)
|
||||
}
|
||||
if !pgCfg.SkipSchemaInit {
|
||||
if schemaErr := broker.EnsureSchema(context.Background()); schemaErr != nil {
|
||||
return nil, fmt.Errorf("error initializing Postgres stream broker schema: %w", schemaErr)
|
||||
}
|
||||
}
|
||||
return broker, nil
|
||||
}
|
||||
|
||||
func createMemoryPresenceManager(n *centrifuge.Node) (centrifuge.PresenceManager, error) {
|
||||
presenceManagerConf, err := memoryPresenceManagerConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return centrifuge.NewMemoryPresenceManager(n, *presenceManagerConf)
|
||||
}
|
||||
|
||||
func createMemoryEngine(n *centrifuge.Node) (centrifuge.Broker, centrifuge.PresenceManager, error) {
|
||||
broker, err := createMemoryBroker(n)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
presenceManager, err := createMemoryPresenceManager(n)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return broker, presenceManager, nil
|
||||
}
|
||||
|
||||
func memoryBrokerConfig() (*centrifuge.MemoryBrokerConfig, error) {
|
||||
return ¢rifuge.MemoryBrokerConfig{}, nil
|
||||
}
|
||||
|
||||
func memoryPresenceManagerConfig() (*centrifuge.MemoryPresenceManagerConfig, error) {
|
||||
return ¢rifuge.MemoryPresenceManagerConfig{}, nil
|
||||
}
|
||||
|
||||
func NatsBroker(node *centrifuge.Node, cfg config.Config) (*natsbroker.NatsBroker, error) {
|
||||
return natsbroker.New(node, cfg.Broker.Nats)
|
||||
}
|
||||
|
||||
func createRedisEngine(n *centrifuge.Node, cfgContainer *config.Container) (*centrifuge.RedisBroker, centrifuge.PresenceManager, string, error) {
|
||||
cfg := cfgContainer.Config()
|
||||
redisShards, mode, err := confighelpers.CentrifugeRedisShards(n, cfg.Engine.Redis.Redis)
|
||||
if err != nil {
|
||||
return nil, nil, mode, fmt.Errorf("error creating Redis shards: %w", err)
|
||||
}
|
||||
|
||||
var broker *centrifuge.RedisBroker
|
||||
if !cfg.Broker.Enabled {
|
||||
broker, err = confighelpers.CentrifugeRedisBroker(
|
||||
n, cfg.Engine.Redis.Prefix, redisShards, cfg.Engine.Redis.RedisBrokerCommon, false)
|
||||
if err != nil {
|
||||
return nil, nil, mode, fmt.Errorf("error creating Redis broker: %w", err)
|
||||
}
|
||||
}
|
||||
var presenceManager centrifuge.PresenceManager
|
||||
if !cfg.PresenceManager.Enabled {
|
||||
presenceManager, err = confighelpers.CentrifugeRedisPresenceManager(
|
||||
n, cfg.Engine.Redis.Prefix, redisShards, cfg.Engine.Redis.RedisPresenceManagerCommon)
|
||||
if err != nil {
|
||||
return nil, nil, mode, fmt.Errorf("error creating Redis presence manager: %w", err)
|
||||
}
|
||||
}
|
||||
return broker, presenceManager, mode, nil
|
||||
}
|
||||
|
||||
func createRedisBroker(n *centrifuge.Node, cfgContainer *config.Container) (*centrifuge.RedisBroker, string, error) {
|
||||
cfg := cfgContainer.Config()
|
||||
redisShards, mode, err := confighelpers.CentrifugeRedisShards(n, cfg.Broker.Redis.Redis)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error creating Redis shards: %w", err)
|
||||
}
|
||||
broker, err := confighelpers.CentrifugeRedisBroker(
|
||||
n, cfg.Broker.Redis.Prefix, redisShards, cfg.Broker.Redis.RedisBrokerCommon, cfg.Broker.Type == "redisnats")
|
||||
if err != nil {
|
||||
return nil, mode, fmt.Errorf("error creating Redis broker: %w", err)
|
||||
}
|
||||
return broker, mode, nil
|
||||
}
|
||||
|
||||
func createRedisPresenceManager(n *centrifuge.Node, cfgContainer *config.Container) (centrifuge.PresenceManager, string, error) {
|
||||
cfg := cfgContainer.Config()
|
||||
redisShards, mode, err := confighelpers.CentrifugeRedisShards(n, cfg.PresenceManager.Redis.Redis)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error creating Redis shards: %w", err)
|
||||
}
|
||||
presenceManager, err := confighelpers.CentrifugeRedisPresenceManager(
|
||||
n, cfg.PresenceManager.Redis.Prefix, redisShards, cfg.PresenceManager.Redis.RedisPresenceManagerCommon)
|
||||
if err != nil {
|
||||
return nil, mode, fmt.Errorf("error creating Redis presence manager: %w", err)
|
||||
}
|
||||
return presenceManager, mode, nil
|
||||
}
|
||||
|
||||
func configureMapBroker(node *centrifuge.Node, cfgContainer *config.Container) error {
|
||||
cfg := cfgContainer.Config()
|
||||
var mapBroker centrifuge.MapBroker
|
||||
var mapBrokerMode string
|
||||
var err error
|
||||
switch cfg.MapBroker.Type {
|
||||
case "memory":
|
||||
mapBroker, err = centrifuge.NewMemoryMapBroker(node, centrifuge.MemoryMapBrokerConfig{})
|
||||
case "redis":
|
||||
var redisShards []*centrifuge.RedisShard
|
||||
redisShards, mapBrokerMode, err = confighelpers.CentrifugeRedisShards(node, cfg.MapBroker.Redis.Redis)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating Redis shards for map broker: %w", err)
|
||||
}
|
||||
mapBroker, err = confighelpers.CentrifugeRedisMapBroker(
|
||||
node, cfg.MapBroker.Redis.Prefix, redisShards, cfg.MapBroker.Redis.RedisMapBrokerCommon)
|
||||
case "postgres":
|
||||
pgCfg := cfg.MapBroker.Postgres
|
||||
pgBrokerCfg := pgmapbroker.PostgresMapBrokerConfig{
|
||||
DSN: pgCfg.DSN,
|
||||
TLS: pgCfg.TLS,
|
||||
PoolSize: pgCfg.PoolSize,
|
||||
NumShards: pgCfg.NumShards,
|
||||
TTLCheckInterval: pgCfg.TTLCheckInterval.ToDuration(),
|
||||
CleanupInterval: pgCfg.CleanupInterval.ToDuration(),
|
||||
IdempotentResultTTL: pgCfg.IdempotentResultTTL.ToDuration(),
|
||||
BinaryData: pgCfg.BinaryData,
|
||||
StreamRetention: pgCfg.StreamRetention.ToDuration(),
|
||||
UseNotify: pgCfg.UseNotify,
|
||||
NotifyDSN: pgCfg.NotifyDSN,
|
||||
TablePrefix: pgCfg.TablePrefix,
|
||||
PartitionLookaheadDays: pgCfg.PartitionLookaheadDays,
|
||||
PartitionRetentionDays: pgCfg.PartitionRetentionDays,
|
||||
Outbox: pgmapbroker.OutboxConfig{
|
||||
PollInterval: pgCfg.Outbox.PollInterval.ToDuration(),
|
||||
BatchSize: pgCfg.Outbox.BatchSize,
|
||||
},
|
||||
}
|
||||
mapBroker, err = pgmapbroker.NewPostgresMapBroker(node, pgBrokerCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating Postgres map broker: %w", err)
|
||||
}
|
||||
if !pgCfg.SkipSchemaInit {
|
||||
pgBroker := mapBroker.(*pgmapbroker.PostgresMapBroker)
|
||||
if schemaErr := pgBroker.EnsureSchema(context.Background()); schemaErr != nil {
|
||||
return fmt.Errorf("error initializing Postgres map broker schema: %w", schemaErr)
|
||||
}
|
||||
}
|
||||
mapBrokerMode = "postgres"
|
||||
default:
|
||||
return fmt.Errorf("unknown map broker type: %s", cfg.MapBroker.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating map broker: %v", err)
|
||||
}
|
||||
event := log.Info().Str("map_broker_type", cfg.MapBroker.Type)
|
||||
if mapBrokerMode != "" {
|
||||
event.Str("map_broker_mode", mapBrokerMode)
|
||||
}
|
||||
event.Msg("initializing map broker")
|
||||
node.SetMapBroker(mapBroker)
|
||||
return nil
|
||||
}
|
||||
23
internal/app/graphite.go
Normal file
23
internal/app/graphite.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/metrics/graphite"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func graphiteExporter(cfg config.Config, nodeCfg centrifuge.Config) *graphite.Exporter {
|
||||
return graphite.New(graphite.Config{
|
||||
Address: net.JoinHostPort(cfg.Graphite.Host, strconv.Itoa(cfg.Graphite.Port)),
|
||||
Gatherer: prometheus.DefaultGatherer,
|
||||
Prefix: strings.TrimSuffix(cfg.Graphite.Prefix, ".") + "." + graphite.PreparePathComponent(nodeCfg.Name),
|
||||
Interval: cfg.Graphite.Interval.ToDuration(),
|
||||
Tags: cfg.Graphite.Tags,
|
||||
})
|
||||
}
|
||||
120
internal/app/grpc.go
Normal file
120
internal/app/grpc.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/api"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/tools"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/unigrpc"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
func runGRPCAPIServer(cfg config.Config, node *centrifuge.Node, useAPIOpentelemetry bool, grpcAPIExecutor *api.Executor) (*grpc.Server, error) {
|
||||
grpcAPIAddr := net.JoinHostPort(cfg.GrpcAPI.Address, strconv.Itoa(cfg.GrpcAPI.Port))
|
||||
grpcAPIConn, err := net.Listen("tcp", grpcAPIAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot listen to address %s", grpcAPIAddr)
|
||||
}
|
||||
var grpcOpts []grpc.ServerOption
|
||||
|
||||
// NOTE: not a bug – GRPC API without a key is intentional and documented behaviour in
|
||||
// Centrifugo v6, see https://centrifugal.dev/docs/server/server_api. Unlike the HTTP API,
|
||||
// GRPC API is disabled by default and runs on its own port, separate from the main HTTP
|
||||
// server, so it's expected to be exposed to an internal network only. It may also be
|
||||
// protected without a key – by mutual TLS via grpc_api.tls, or JWKS-based auth in PRO.
|
||||
// Do not make an empty key fail-closed here: it silently breaks existing setups. In v7 we
|
||||
// may require at least one auth method unless an explicit insecure flag is set.
|
||||
if cfg.GrpcAPI.Key != "" {
|
||||
grpcOpts = append(grpcOpts, api.GRPCKeyAuth(cfg.GrpcAPI.Key))
|
||||
}
|
||||
if cfg.GrpcAPI.MaxReceiveMessageSize > 0 {
|
||||
grpcOpts = append(grpcOpts, grpc.MaxRecvMsgSize(cfg.GrpcAPI.MaxReceiveMessageSize))
|
||||
}
|
||||
var grpcAPITLSConfig *tls.Config
|
||||
if cfg.GrpcAPI.TLS.Enabled {
|
||||
grpcAPITLSConfig, err = cfg.GrpcAPI.TLS.ToGoTLSConfig("grpc_api")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting TLS config for GRPC API: %v", err)
|
||||
}
|
||||
}
|
||||
if grpcAPITLSConfig != nil {
|
||||
grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(grpcAPITLSConfig)))
|
||||
}
|
||||
if cfg.OpenTelemetry.Enabled && cfg.OpenTelemetry.API {
|
||||
grpcOpts = append(grpcOpts, grpc.StatsHandler(otelgrpc.NewServerHandler()))
|
||||
}
|
||||
grpcErrorMode, err := tools.OptionalStringChoice(cfg.GrpcAPI.ErrorMode, []string{config.TransportErrorMode})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't extract grpc error mode: %v", err)
|
||||
}
|
||||
grpcAPIServer := grpc.NewServer(grpcOpts...)
|
||||
_ = api.RegisterGRPCServerAPI(node, grpcAPIExecutor, grpcAPIServer, api.GRPCAPIServiceConfig{
|
||||
UseOpenTelemetry: useAPIOpentelemetry,
|
||||
UseTransportErrorMode: grpcErrorMode == config.TransportErrorMode,
|
||||
})
|
||||
if cfg.GrpcAPI.Reflection {
|
||||
reflection.Register(grpcAPIServer)
|
||||
}
|
||||
log.Info().Msgf("serving GRPC API service on %s", grpcAPIAddr)
|
||||
go func() {
|
||||
if err := grpcAPIServer.Serve(grpcAPIConn); err != nil {
|
||||
log.Fatal().Err(err).Msg("serve GRPC API")
|
||||
}
|
||||
}()
|
||||
return grpcAPIServer, nil
|
||||
}
|
||||
|
||||
func runGRPCUniServer(cfg config.Config, node *centrifuge.Node) (*grpc.Server, error) {
|
||||
grpcUniAddr := net.JoinHostPort(cfg.UniGRPC.Address, strconv.Itoa(cfg.UniGRPC.Port))
|
||||
grpcUniConn, err := net.Listen("tcp", grpcUniAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot listen to address %s", grpcUniAddr)
|
||||
}
|
||||
var grpcOpts []grpc.ServerOption
|
||||
grpcOpts = append(grpcOpts, grpc.ForceServerCodec(&unigrpc.RawCodec{}))
|
||||
|
||||
if cfg.UniGRPC.MaxReceiveMessageSize > 0 {
|
||||
grpcOpts = append(grpcOpts, grpc.MaxRecvMsgSize(cfg.UniGRPC.MaxReceiveMessageSize))
|
||||
}
|
||||
|
||||
var uniGrpcTLSConfig *tls.Config
|
||||
if cfg.UniGRPC.TLS.Enabled {
|
||||
uniGrpcTLSConfig, err = cfg.UniGRPC.TLS.ToGoTLSConfig("uni_grpc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting TLS config for uni GRPC: %v", err)
|
||||
}
|
||||
}
|
||||
if uniGrpcTLSConfig != nil {
|
||||
grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(uniGrpcTLSConfig)))
|
||||
}
|
||||
keepAliveEnforcementPolicy := keepalive.EnforcementPolicy{
|
||||
MinTime: 5 * time.Second,
|
||||
}
|
||||
keepAliveServerParams := keepalive.ServerParameters{
|
||||
Time: 25 * time.Second,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
grpcOpts = append(grpcOpts, grpc.KeepaliveEnforcementPolicy(keepAliveEnforcementPolicy))
|
||||
grpcOpts = append(grpcOpts, grpc.KeepaliveParams(keepAliveServerParams))
|
||||
grpcUniServer := grpc.NewServer(grpcOpts...)
|
||||
_ = unigrpc.RegisterService(grpcUniServer, unigrpc.NewService(node, cfg.UniGRPC))
|
||||
log.Info().Msgf("serving unidirectional GRPC on %s", grpcUniAddr)
|
||||
go func() {
|
||||
if err := grpcUniServer.Serve(grpcUniConn); err != nil {
|
||||
log.Fatal().Err(err).Msg("serve uni GRPC")
|
||||
}
|
||||
}()
|
||||
return grpcUniServer, nil
|
||||
}
|
||||
47
internal/app/log.go
Normal file
47
internal/app/log.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func logStartWarnings(cfg config.Config, cfgMeta config.Meta) {
|
||||
if cfg.Client.Insecure {
|
||||
log.Warn().Msg("INSECURE client mode enabled, make sure you understand risks")
|
||||
}
|
||||
if cfg.HttpAPI.Insecure {
|
||||
log.Warn().Msg("INSECURE HTTP API mode enabled, make sure you understand risks")
|
||||
}
|
||||
if cfg.Admin.Insecure {
|
||||
log.Warn().Msg("INSECURE admin mode enabled, make sure you understand risks")
|
||||
}
|
||||
if cfg.Client.InsecureSkipTokenSignatureVerify {
|
||||
log.Warn().Msg("INSECURE skip token signature verify enabled, make sure you understand risks")
|
||||
}
|
||||
if cfg.Debug.Enabled {
|
||||
log.Warn().Msg("DEBUG mode enabled, see on " + cfg.Debug.HandlerPrefix)
|
||||
}
|
||||
|
||||
for _, key := range cfgMeta.UnknownKeys {
|
||||
log.Warn().Str("key", key).Msg("unknown key in configuration file")
|
||||
}
|
||||
for _, key := range cfgMeta.UnknownEnvs {
|
||||
log.Warn().Str("var", key).Msg("unknown var in environment")
|
||||
}
|
||||
for _, msg := range cfgMeta.DeprecationWarnings {
|
||||
log.Warn().Msg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
type httpErrorLogWriter struct {
|
||||
zerolog.Logger
|
||||
}
|
||||
|
||||
func (w *httpErrorLogWriter) Write(data []byte) (int, error) {
|
||||
w.Logger.Warn().Msg(strings.TrimSpace(string(data)))
|
||||
return len(data), nil
|
||||
}
|
||||
635
internal/app/mux.go
Normal file
635
internal/app/mux.go
Normal file
@@ -0,0 +1,635 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/admin"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/api"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/conninit"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/health"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/middleware"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/swaggerui"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/tools"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/unihttpstream"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/unisse"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/uniws"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/wt"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/justinas/alice"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/quic-go/webtransport-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// HandlerFlag is a bit mask of handlers that must be enabled in mux.
|
||||
type HandlerFlag int
|
||||
|
||||
const (
|
||||
// HandlerWebsocket enables Raw Websocket handler.
|
||||
HandlerWebsocket HandlerFlag = 1 << iota
|
||||
// HandlerWebtransport enables Webtransport handler (requires HTTP/3)
|
||||
HandlerWebtransport
|
||||
// HandlerAPI enables API handler.
|
||||
HandlerAPI
|
||||
// HandlerAdmin enables admin web interface.
|
||||
HandlerAdmin
|
||||
// HandlerDebug enables debug handlers.
|
||||
HandlerDebug
|
||||
// HandlerPrometheus enables Prometheus handler.
|
||||
HandlerPrometheus
|
||||
// HandlerHealth enables Health check endpoint.
|
||||
HandlerHealth
|
||||
// HandlerUniWebsocket enables unidirectional websocket endpoint.
|
||||
HandlerUniWebsocket
|
||||
// HandlerUniSSE enables unidirectional SSE endpoint.
|
||||
HandlerUniSSE
|
||||
// HandlerUniHTTPStream enables unidirectional HTTP stream endpoint.
|
||||
HandlerUniHTTPStream
|
||||
// HandlerSSE enables bidirectional SSE endpoint (with emulation layer).
|
||||
HandlerSSE
|
||||
// HandlerHTTPStream enables bidirectional HTTP stream endpoint (with emulation layer).
|
||||
HandlerHTTPStream
|
||||
// HandlerEmulation handles client-to-server requests in an emulation layer.
|
||||
HandlerEmulation
|
||||
// HandlerInit handles connection initialization.
|
||||
HandlerInit
|
||||
// HandlerSwagger handles swagger UI.
|
||||
HandlerSwagger
|
||||
)
|
||||
|
||||
var handlerText = map[HandlerFlag]string{
|
||||
HandlerWebsocket: "websocket",
|
||||
HandlerWebtransport: "webtransport",
|
||||
HandlerAPI: "api",
|
||||
HandlerAdmin: "admin",
|
||||
HandlerDebug: "debug",
|
||||
HandlerPrometheus: "prometheus",
|
||||
HandlerHealth: "health",
|
||||
HandlerUniWebsocket: "uni_websocket",
|
||||
HandlerUniSSE: "uni_sse",
|
||||
HandlerUniHTTPStream: "uni_http_stream",
|
||||
HandlerSSE: "sse",
|
||||
HandlerHTTPStream: "http_stream",
|
||||
HandlerEmulation: "emulation",
|
||||
HandlerInit: "init",
|
||||
HandlerSwagger: "swagger",
|
||||
}
|
||||
|
||||
func (flags HandlerFlag) String() string {
|
||||
flagsOrdered := []HandlerFlag{HandlerWebsocket, HandlerWebtransport, HandlerHTTPStream, HandlerSSE, HandlerEmulation, HandlerAPI, HandlerAdmin, HandlerPrometheus, HandlerDebug, HandlerHealth, HandlerUniWebsocket, HandlerUniSSE, HandlerUniHTTPStream, HandlerSwagger, HandlerInit}
|
||||
var endpoints []string
|
||||
for _, flag := range flagsOrdered {
|
||||
text, ok := handlerText[flag]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if flags&flag != 0 {
|
||||
endpoints = append(endpoints, text)
|
||||
}
|
||||
}
|
||||
return strings.Join(endpoints, ", ")
|
||||
}
|
||||
|
||||
// Mux returns a mux including set of default handlers for Centrifugo server.
|
||||
func Mux(
|
||||
n *centrifuge.Node, cfgContainer *config.Container, apiExecutor *api.Executor, flags HandlerFlag, keepHeadersInContext bool, wtServer *webtransport.Server,
|
||||
) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
cfg := cfgContainer.Config()
|
||||
|
||||
var commonMiddlewares []alice.Constructor
|
||||
|
||||
useLoggingMW := zerolog.GlobalLevel() <= zerolog.DebugLevel
|
||||
if useLoggingMW {
|
||||
commonMiddlewares = append(commonMiddlewares, middleware.LogRequest)
|
||||
}
|
||||
if cfg.Prometheus.Enabled && cfg.Prometheus.InstrumentHTTPHandlers {
|
||||
commonMiddlewares = append(commonMiddlewares, middleware.HTTPServerInstrumentation)
|
||||
}
|
||||
|
||||
basicMiddlewares := append([]alice.Constructor{}, commonMiddlewares...)
|
||||
basicChain := alice.New(basicMiddlewares...)
|
||||
|
||||
if flags&HandlerDebug != 0 {
|
||||
mux.Handle(cfg.Debug.HandlerPrefix+"/", basicChain.Then(http.HandlerFunc(pprof.Index)))
|
||||
mux.Handle(cfg.Debug.HandlerPrefix+"/cmdline", basicChain.Then(http.HandlerFunc(pprof.Cmdline)))
|
||||
mux.Handle(cfg.Debug.HandlerPrefix+"/profile", basicChain.Then(http.HandlerFunc(pprof.Profile)))
|
||||
mux.Handle(cfg.Debug.HandlerPrefix+"/symbol", basicChain.Then(http.HandlerFunc(pprof.Symbol)))
|
||||
mux.Handle(cfg.Debug.HandlerPrefix+"/trace", basicChain.Then(http.HandlerFunc(pprof.Trace)))
|
||||
}
|
||||
|
||||
if flags&HandlerEmulation != 0 {
|
||||
// register bidirectional emulation endpoint.
|
||||
emulationMiddlewares := append([]alice.Constructor{}, commonMiddlewares...)
|
||||
emulationMiddlewares = append(emulationMiddlewares, middleware.NewCORS(getCheckOrigin(cfg)).Middleware)
|
||||
emulationChain := alice.New(emulationMiddlewares...)
|
||||
|
||||
emulationPrefix := strings.TrimRight(cfg.Emulation.HandlerPrefix, "/")
|
||||
if emulationPrefix == "" {
|
||||
emulationPrefix = "/"
|
||||
}
|
||||
mux.Handle(emulationPrefix, emulationChain.Then(centrifuge.NewEmulationHandler(n, emulationHandlerConfig(cfg))))
|
||||
}
|
||||
|
||||
connMiddlewares := append([]alice.Constructor{}, commonMiddlewares...)
|
||||
// The ConnLimit middleware enforces both the total connection cap and the
|
||||
// connection rate limit, so install it when either is configured - otherwise
|
||||
// connection_rate_limit is silently ignored when connection_limit is 0.
|
||||
if cfg.Client.ConnectionLimit > 0 || cfg.Client.ConnectionRateLimit > 0 {
|
||||
connLimitMW := middleware.NewConnLimit(n, cfgContainer)
|
||||
connMiddlewares = append(connMiddlewares, connLimitMW.Middleware)
|
||||
}
|
||||
userIDHTTPHeader := cfg.Client.UserIDHTTPHeader
|
||||
if userIDHTTPHeader != "" {
|
||||
connMiddlewares = append(connMiddlewares, middleware.UserHeaderAuth(userIDHTTPHeader))
|
||||
}
|
||||
if keepHeadersInContext {
|
||||
connMiddlewares = append(connMiddlewares, middleware.HeadersToContext)
|
||||
}
|
||||
connMiddlewares = append(connMiddlewares, middleware.NewCORS(getCheckOrigin(cfg)).Middleware)
|
||||
connChain := alice.New(connMiddlewares...)
|
||||
|
||||
if flags&HandlerWebsocket != 0 {
|
||||
// register WebSocket connection endpoint.
|
||||
wsPrefix := strings.TrimRight(cfg.WebSocket.HandlerPrefix, "/")
|
||||
if wsPrefix == "" {
|
||||
wsPrefix = "/"
|
||||
}
|
||||
mux.Handle(wsPrefix, connChain.Then(centrifuge.NewWebsocketHandler(n, websocketHandlerConfig(cfg))))
|
||||
}
|
||||
|
||||
if flags&HandlerWebtransport != 0 {
|
||||
// register WebTransport connection endpoint.
|
||||
wtPrefix := strings.TrimRight(cfg.WebTransport.HandlerPrefix, "/")
|
||||
if wtPrefix == "" {
|
||||
wtPrefix = "/"
|
||||
}
|
||||
mux.Handle(wtPrefix, connChain.Then(wt.NewHandler(n, wtServer, cfg.WebTransport, getPingPongConfig(cfg))))
|
||||
}
|
||||
|
||||
if flags&HandlerInit != 0 {
|
||||
// register connection initialization endpoint.
|
||||
connInitPrefix := strings.TrimRight(cfg.Init.HandlerPrefix, "/")
|
||||
if connInitPrefix == "" {
|
||||
connInitPrefix = "/"
|
||||
}
|
||||
mux.Handle(connInitPrefix, middleware.Method(http.MethodGet, connChain.Then(conninit.NewHandler())))
|
||||
}
|
||||
|
||||
if flags&HandlerHTTPStream != 0 {
|
||||
// register bidirectional HTTP stream connection endpoint.
|
||||
streamPrefix := strings.TrimRight(cfg.HTTPStream.HandlerPrefix, "/")
|
||||
if streamPrefix == "" {
|
||||
streamPrefix = "/"
|
||||
}
|
||||
mux.Handle(streamPrefix, connChain.Then(centrifuge.NewHTTPStreamHandler(n, httpStreamHandlerConfig(cfg))))
|
||||
}
|
||||
if flags&HandlerSSE != 0 {
|
||||
// register bidirectional SSE connection endpoint.
|
||||
ssePrefix := strings.TrimRight(cfg.SSE.HandlerPrefix, "/")
|
||||
if ssePrefix == "" {
|
||||
ssePrefix = "/"
|
||||
}
|
||||
mux.Handle(ssePrefix, connChain.Then(centrifuge.NewSSEHandler(n, sseHandlerConfig(cfg))))
|
||||
}
|
||||
|
||||
if flags&HandlerUniWebsocket != 0 {
|
||||
// register unidirectional WebSocket connection endpoint.
|
||||
wsPrefix := strings.TrimRight(cfg.UniWS.HandlerPrefix, "/")
|
||||
if wsPrefix == "" {
|
||||
wsPrefix = "/"
|
||||
}
|
||||
mux.Handle(wsPrefix, connChain.Then(
|
||||
uniws.NewHandler(n, cfg.UniWS, getCheckOrigin(cfg), getPingPongConfig(cfg))))
|
||||
}
|
||||
|
||||
if flags&HandlerUniSSE != 0 {
|
||||
// register unidirectional SSE connection endpoint.
|
||||
ssePrefix := strings.TrimRight(cfg.UniSSE.HandlerPrefix, "/")
|
||||
if ssePrefix == "" {
|
||||
ssePrefix = "/"
|
||||
}
|
||||
mux.Handle(ssePrefix, connChain.Then(unisse.NewHandler(n, cfg.UniSSE, getPingPongConfig(cfg))))
|
||||
}
|
||||
|
||||
if flags&HandlerUniHTTPStream != 0 {
|
||||
// register unidirectional HTTP stream connection endpoint.
|
||||
streamPrefix := strings.TrimRight(cfg.UniHTTPStream.HandlerPrefix, "/")
|
||||
if streamPrefix == "" {
|
||||
streamPrefix = "/"
|
||||
}
|
||||
mux.Handle(streamPrefix, connChain.Then(unihttpstream.NewHandler(n, cfg.UniHTTPStream, getPingPongConfig(cfg))))
|
||||
}
|
||||
|
||||
if flags&HandlerAPI != 0 {
|
||||
// register HTTP API endpoints.
|
||||
httpErrorMode, err := tools.OptionalStringChoice(cfg.HttpAPI.ErrorMode, []string{config.TransportErrorMode})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error in config")
|
||||
}
|
||||
useOpenTelemetry := cfg.OpenTelemetry.Enabled && cfg.OpenTelemetry.API
|
||||
apiHandler := api.NewHandler(n, apiExecutor, api.Config{
|
||||
UseOpenTelemetry: useOpenTelemetry,
|
||||
UseTransportErrorMode: httpErrorMode == config.TransportErrorMode,
|
||||
})
|
||||
apiPrefix := strings.TrimRight(cfg.HttpAPI.HandlerPrefix, "/")
|
||||
if apiPrefix == "" {
|
||||
apiPrefix = "/"
|
||||
}
|
||||
|
||||
apiChain := func(op string) alice.Chain {
|
||||
apiMiddlewares := append([]alice.Constructor{}, commonMiddlewares...)
|
||||
otelHandler := middleware.NewOpenTelemetryHandler(op, nil)
|
||||
if useOpenTelemetry {
|
||||
apiMiddlewares = append(apiMiddlewares, otelHandler.Middleware)
|
||||
}
|
||||
apiMiddlewares = append(apiMiddlewares, middleware.Post)
|
||||
if !cfg.HttpAPI.Insecure {
|
||||
apiMiddlewares = append(apiMiddlewares, middleware.NewAPIKeyAuth(cfg.HttpAPI.Key).Middleware)
|
||||
}
|
||||
apiChain := alice.New(apiMiddlewares...)
|
||||
return apiChain
|
||||
}
|
||||
|
||||
mux.Handle(apiPrefix, apiChain(apiPrefix).Then(apiHandler.OldRoute()))
|
||||
if apiPrefix != "/" {
|
||||
for path, handler := range apiHandler.Routes() {
|
||||
handlePath := apiPrefix + path
|
||||
mux.Handle(handlePath, apiChain(handlePath).Then(handler))
|
||||
}
|
||||
} else {
|
||||
for path, handler := range apiHandler.Routes() {
|
||||
mux.Handle(path, apiChain(path).Then(handler))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if flags&HandlerSwagger != 0 {
|
||||
// register Swagger UI endpoint.
|
||||
swaggerPrefix := strings.TrimRight(cfg.Swagger.HandlerPrefix, "/") + "/"
|
||||
if swaggerPrefix == "" {
|
||||
swaggerPrefix = "/"
|
||||
}
|
||||
mux.Handle(swaggerPrefix, basicChain.Then(http.StripPrefix(swaggerPrefix, http.FileServer(swaggerui.FS))))
|
||||
}
|
||||
|
||||
if flags&HandlerPrometheus != 0 {
|
||||
// register Prometheus metrics export endpoint.
|
||||
prometheusPrefix := strings.TrimRight(cfg.Prometheus.HandlerPrefix, "/")
|
||||
if prometheusPrefix == "" {
|
||||
prometheusPrefix = "/"
|
||||
}
|
||||
mux.Handle(prometheusPrefix, basicChain.Then(promhttp.Handler()))
|
||||
}
|
||||
|
||||
if flags&HandlerAdmin != 0 {
|
||||
adminPrefix := strings.TrimRight(cfg.Admin.HandlerPrefix, "/")
|
||||
mux.Handle(adminPrefix+"/", basicChain.Then(admin.NewHandler(n, apiExecutor, cfg.Admin)))
|
||||
}
|
||||
|
||||
if flags&HandlerHealth != 0 {
|
||||
healthPrefix := strings.TrimRight(cfg.Health.HandlerPrefix, "/")
|
||||
if healthPrefix == "" {
|
||||
healthPrefix = "/"
|
||||
}
|
||||
mux.Handle(healthPrefix, basicChain.Then(health.NewHandler(n, health.Config{})))
|
||||
}
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func getPingPongConfig(cfg config.Config) centrifuge.PingPongConfig {
|
||||
pingInterval := cfg.Client.PingInterval
|
||||
pongTimeout := cfg.Client.PongTimeout
|
||||
if pingInterval <= pongTimeout {
|
||||
log.Fatal().Str("ping_interval", pingInterval.String()).Str("pong_timeout", pongTimeout.String()).
|
||||
Msg("ping_interval must be greater than pong_timeout")
|
||||
}
|
||||
return centrifuge.PingPongConfig{
|
||||
PingInterval: pingInterval.ToDuration(),
|
||||
PongTimeout: pongTimeout.ToDuration(),
|
||||
}
|
||||
}
|
||||
|
||||
func websocketHandlerConfig(appCfg config.Config) centrifuge.WebsocketConfig {
|
||||
cfg := centrifuge.WebsocketConfig{}
|
||||
cfg.Compression = appCfg.WebSocket.Compression
|
||||
cfg.CompressionLevel = appCfg.WebSocket.CompressionLevel
|
||||
cfg.CompressionMinSize = appCfg.WebSocket.CompressionMinSize
|
||||
cfg.ReadBufferSize = appCfg.WebSocket.ReadBufferSize
|
||||
cfg.WriteBufferSize = appCfg.WebSocket.WriteBufferSize
|
||||
cfg.UseWriteBufferPool = appCfg.WebSocket.UseWriteBufferPool
|
||||
cfg.WriteTimeout = appCfg.WebSocket.WriteTimeout.ToDuration()
|
||||
cfg.MessageSizeLimit = appCfg.WebSocket.MessageSizeLimit
|
||||
cfg.DecompressedMessageSizeLimit = appCfg.WebSocket.DecompressedMessageSizeLimit
|
||||
cfg.CheckOrigin = getCheckOrigin(appCfg)
|
||||
cfg.PingPongConfig = getPingPongConfig(appCfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func httpStreamHandlerConfig(appCfg config.Config) centrifuge.HTTPStreamConfig {
|
||||
return centrifuge.HTTPStreamConfig{
|
||||
MaxRequestBodySize: appCfg.HTTPStream.MaxRequestBodySize,
|
||||
PingPongConfig: getPingPongConfig(appCfg),
|
||||
}
|
||||
}
|
||||
|
||||
func sseHandlerConfig(appCfg config.Config) centrifuge.SSEConfig {
|
||||
return centrifuge.SSEConfig{
|
||||
MaxRequestBodySize: appCfg.SSE.MaxRequestBodySize,
|
||||
PingPongConfig: getPingPongConfig(appCfg),
|
||||
}
|
||||
}
|
||||
|
||||
func emulationHandlerConfig(cfg config.Config) centrifuge.EmulationConfig {
|
||||
return centrifuge.EmulationConfig{
|
||||
MaxRequestBodySize: cfg.Emulation.MaxRequestBodySize,
|
||||
}
|
||||
}
|
||||
|
||||
func runHTTPServers(
|
||||
n *centrifuge.Node, cfgContainer *config.Container, apiExecutor *api.Executor, keepHeadersInContext bool,
|
||||
) ([]*http.Server, error) {
|
||||
cfg := cfgContainer.Config()
|
||||
|
||||
debug := cfg.Debug.Enabled
|
||||
useAdmin := cfg.Admin.Enabled
|
||||
usePrometheus := cfg.Prometheus.Enabled
|
||||
useHealth := cfg.Health.Enabled
|
||||
useSwagger := cfg.Swagger.Enabled
|
||||
useConnInit := cfg.Init.Enabled
|
||||
|
||||
adminExternal := cfg.Admin.External
|
||||
apiExternal := cfg.HttpAPI.External
|
||||
|
||||
apiDisabled := cfg.HttpAPI.Disabled
|
||||
|
||||
httpAddress := cfg.HTTP.Address
|
||||
httpPort := strconv.Itoa(cfg.HTTP.Port)
|
||||
httpInternalAddress := cfg.HTTP.InternalAddress
|
||||
httpInternalPort := cfg.HTTP.InternalPort
|
||||
|
||||
if httpInternalAddress == "" && httpAddress != "" {
|
||||
// If custom internal address not explicitly set we try to reuse main
|
||||
// address for internal endpoints too.
|
||||
httpInternalAddress = httpAddress
|
||||
}
|
||||
|
||||
if httpInternalPort == "" {
|
||||
// If custom internal port not set we use default http port for
|
||||
// internal endpoints too.
|
||||
httpInternalPort = httpPort
|
||||
}
|
||||
|
||||
// addrToHandlerFlags contains mapping between HTTP server address and
|
||||
// handler flags to serve on this address.
|
||||
addrToHandlerFlags := map[string]HandlerFlag{}
|
||||
|
||||
var portFlags HandlerFlag
|
||||
|
||||
externalAddr := net.JoinHostPort(httpAddress, httpPort)
|
||||
portFlags = addrToHandlerFlags[externalAddr]
|
||||
if !cfg.WebSocket.Disabled {
|
||||
portFlags |= HandlerWebsocket
|
||||
}
|
||||
if cfg.WebTransport.Enabled {
|
||||
if !cfg.HTTP.HTTP3.Enabled {
|
||||
log.Fatal().Msg("can not enable webtransport without experimental HTTP/3")
|
||||
}
|
||||
portFlags |= HandlerWebtransport
|
||||
}
|
||||
if cfg.SSE.Enabled {
|
||||
portFlags |= HandlerSSE
|
||||
}
|
||||
if cfg.HTTPStream.Enabled {
|
||||
portFlags |= HandlerHTTPStream
|
||||
}
|
||||
if cfg.SSE.Enabled || cfg.HTTPStream.Enabled {
|
||||
portFlags |= HandlerEmulation
|
||||
}
|
||||
if useAdmin && adminExternal {
|
||||
portFlags |= HandlerAdmin
|
||||
}
|
||||
if !apiDisabled && apiExternal {
|
||||
portFlags |= HandlerAPI
|
||||
}
|
||||
if cfg.UniWS.Enabled {
|
||||
portFlags |= HandlerUniWebsocket
|
||||
}
|
||||
if cfg.UniSSE.Enabled {
|
||||
portFlags |= HandlerUniSSE
|
||||
}
|
||||
if cfg.UniHTTPStream.Enabled {
|
||||
portFlags |= HandlerUniHTTPStream
|
||||
}
|
||||
if useConnInit {
|
||||
portFlags |= HandlerInit
|
||||
}
|
||||
addrToHandlerFlags[externalAddr] = portFlags
|
||||
|
||||
internalAddr := net.JoinHostPort(httpInternalAddress, httpInternalPort)
|
||||
portFlags = addrToHandlerFlags[internalAddr]
|
||||
if !apiDisabled && !apiExternal {
|
||||
portFlags |= HandlerAPI
|
||||
}
|
||||
|
||||
if useAdmin && !adminExternal {
|
||||
portFlags |= HandlerAdmin
|
||||
}
|
||||
if usePrometheus {
|
||||
portFlags |= HandlerPrometheus
|
||||
}
|
||||
if useSwagger {
|
||||
portFlags |= HandlerSwagger
|
||||
}
|
||||
if debug {
|
||||
portFlags |= HandlerDebug
|
||||
}
|
||||
if useHealth {
|
||||
portFlags |= HandlerHealth
|
||||
}
|
||||
addrToHandlerFlags[internalAddr] = portFlags
|
||||
|
||||
var servers []*http.Server
|
||||
|
||||
tlsConfig, err := GetTLSConfig(cfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("can not get TLS config")
|
||||
}
|
||||
var internalTLSConfig *tls.Config
|
||||
if cfg.HTTP.InternalTLS.Enabled {
|
||||
internalTLSConfig, err = cfg.HTTP.InternalTLS.ToGoTLSConfig("internal_tls")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("can not get internal TLS config")
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over port-to-flags mapping and start HTTP servers
|
||||
// on separate ports serving handlers specified in flags.
|
||||
for addr, handlerFlags := range addrToHandlerFlags {
|
||||
addr := addr
|
||||
if handlerFlags == 0 {
|
||||
continue
|
||||
}
|
||||
var addrTLSConfig *tls.Config
|
||||
if !cfg.HTTP.TLSExternal || addr == externalAddr {
|
||||
addrTLSConfig = tlsConfig
|
||||
}
|
||||
if addr != externalAddr && cfg.HTTP.InternalTLS.Enabled {
|
||||
addrTLSConfig = internalTLSConfig
|
||||
}
|
||||
|
||||
useHTTP3 := cfg.HTTP.HTTP3.Enabled && addr == externalAddr
|
||||
useWebtransport := useHTTP3 && handlerFlags&HandlerWebtransport != 0
|
||||
|
||||
var wtServer *webtransport.Server
|
||||
if useWebtransport {
|
||||
wtServer = &webtransport.Server{
|
||||
CheckOrigin: getCheckOrigin(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
mux := Mux(n, cfgContainer, apiExecutor, handlerFlags, keepHeadersInContext, wtServer)
|
||||
|
||||
var h3Server *http3.Server
|
||||
if useHTTP3 {
|
||||
h3Server = &http3.Server{
|
||||
Addr: addr,
|
||||
TLSConfig: addrTLSConfig,
|
||||
Handler: mux,
|
||||
}
|
||||
if useWebtransport {
|
||||
webtransport.ConfigureHTTP3Server(h3Server)
|
||||
wtServer.H3 = h3Server
|
||||
}
|
||||
}
|
||||
|
||||
var protoSuffix string
|
||||
if useHTTP3 {
|
||||
protoSuffix = " with HTTP/3 (experimental)"
|
||||
}
|
||||
|
||||
useH2C := cfg.HTTP.H2CExternal && addr == externalAddr && addrTLSConfig == nil
|
||||
if useH2C {
|
||||
protoSuffix = " with HTTP/2 cleartext"
|
||||
}
|
||||
|
||||
log.Info().Msgf("serving %s endpoints on %s%s", handlerFlags, addr, protoSuffix)
|
||||
|
||||
var handler http.Handler = mux
|
||||
if useHTTP3 {
|
||||
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = h3Server.SetQUICHeaders(w.Header())
|
||||
mux.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
TLSConfig: addrTLSConfig,
|
||||
ErrorLog: stdlog.New(&httpErrorLogWriter{Logger: log.Logger}, "", 0),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
if useH2C {
|
||||
protocols := new(http.Protocols)
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
server.Protocols = protocols
|
||||
}
|
||||
|
||||
servers = append(servers, server)
|
||||
|
||||
go func() {
|
||||
if useHTTP3 {
|
||||
if addrTLSConfig == nil {
|
||||
log.Fatal().Msg("HTTP/3 requires TLS configured")
|
||||
}
|
||||
if cfg.HTTP.TLSAutocert.Enabled {
|
||||
log.Fatal().Msg("can not use HTTP/3 with autocert")
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("can not start HTTP/3, resolve UDP")
|
||||
}
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("can not start HTTP/3, listen UDP")
|
||||
}
|
||||
defer func() { _ = udpConn.Close() }()
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("can not start HTTP/3, resolve TCP")
|
||||
}
|
||||
tcpConn, err := net.ListenTCP("tcp", tcpAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("can not start HTTP/3, listen TCP")
|
||||
}
|
||||
defer func() { _ = tcpConn.Close() }()
|
||||
|
||||
tlsConn := tls.NewListener(tcpConn, addrTLSConfig)
|
||||
defer func() { _ = tlsConn.Close() }()
|
||||
|
||||
hErr := make(chan error)
|
||||
qErr := make(chan error)
|
||||
go func() {
|
||||
hErr <- server.Serve(tlsConn)
|
||||
}()
|
||||
go func() {
|
||||
if useWebtransport {
|
||||
qErr <- wtServer.Serve(udpConn)
|
||||
} else {
|
||||
qErr <- h3Server.Serve(udpConn)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-hErr:
|
||||
if useWebtransport {
|
||||
_ = wtServer.Close()
|
||||
} else {
|
||||
_ = h3Server.Close()
|
||||
}
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal().Err(err).Msg("error ListenAndServe")
|
||||
}
|
||||
case err := <-qErr:
|
||||
// Cannot close the HTTP server or wait for requests to complete properly.
|
||||
log.Fatal().Err(err).Msg("error ListenAndServe HTTP/3")
|
||||
}
|
||||
} else {
|
||||
if addrTLSConfig != nil {
|
||||
if err := server.ListenAndServeTLS("", ""); err != nil {
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal().Err(err).Msg("error ListenAndServeTLS")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal().Err(err).Msgf("error ListenAndServe")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
122
internal/app/node.go
Normal file
122
internal/app/node.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/logging"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
)
|
||||
|
||||
func centrifugeNodeConfig(version string, edition string, cfgContainer *config.Container, logHandler centrifuge.LogHandler) centrifuge.Config {
|
||||
appCfg := cfgContainer.Config()
|
||||
cfg := centrifuge.Config{}
|
||||
cfg.Version = version + " " + strings.ToUpper(edition)
|
||||
cfg.Metrics = centrifuge.MetricsConfig{
|
||||
MetricsNamespace: "centrifugo",
|
||||
EnableRecoveredPublicationsHistogram: appCfg.Prometheus.RecoveredPublicationsHistogram,
|
||||
EnableNativeHistograms: appCfg.Prometheus.NativeHistograms,
|
||||
}
|
||||
cfg.Name = nodeName(appCfg)
|
||||
cfg.ChannelMaxLength = appCfg.Channel.MaxLength
|
||||
cfg.ClientPresenceUpdateInterval = appCfg.Client.PresenceUpdateInterval.ToDuration()
|
||||
cfg.ClientExpiredCloseDelay = appCfg.Client.ExpiredCloseDelay.ToDuration()
|
||||
cfg.ClientExpiredSubCloseDelay = appCfg.Client.ExpiredSubCloseDelay.ToDuration()
|
||||
cfg.ClientStaleCloseDelay = appCfg.Client.StaleCloseDelay.ToDuration()
|
||||
cfg.ClientQueueMaxSize = appCfg.Client.QueueMaxSize
|
||||
cfg.ClientChannelLimit = appCfg.Client.ChannelLimit
|
||||
cfg.ClientChannelPositionCheckDelay = appCfg.Client.ChannelPositionCheckDelay.ToDuration()
|
||||
cfg.ClientChannelPositionMaxTimeLag = appCfg.Client.ChannelPositionMaxTimeLag.ToDuration()
|
||||
cfg.UserConnectionLimit = appCfg.Client.UserConnectionLimit
|
||||
cfg.NodeInfoMetricsAggregateInterval = appCfg.Node.InfoMetricsAggregateInterval.ToDuration()
|
||||
cfg.HistoryMaxPublicationLimit = appCfg.Client.HistoryMaxPublicationLimit
|
||||
cfg.RecoveryMaxPublicationLimit = appCfg.Client.RecoveryMaxPublicationLimit
|
||||
cfg.Map.GetMapChannelOptions = func(channel string) centrifuge.MapChannelOptions {
|
||||
_, _, chOpts, ok, err := cfgContainer.ChannelOptions(channel)
|
||||
if err != nil || !ok {
|
||||
return centrifuge.MapChannelOptions{}
|
||||
}
|
||||
var mode centrifuge.MapMode
|
||||
switch chOpts.Map.Mode {
|
||||
case "ephemeral":
|
||||
mode = centrifuge.MapModeEphemeral
|
||||
case "recoverable":
|
||||
mode = centrifuge.MapModeRecoverable
|
||||
case "persistent":
|
||||
mode = centrifuge.MapModePersistent
|
||||
}
|
||||
return centrifuge.MapChannelOptions{
|
||||
Mode: mode,
|
||||
KeyTTL: chOpts.Map.KeyTTL.ToDuration(),
|
||||
StreamSize: chOpts.Map.StreamSize,
|
||||
StreamTTL: chOpts.Map.StreamTTL.ToDuration(),
|
||||
MetaTTL: chOpts.Map.MetaTTL.ToDuration(),
|
||||
DefaultPageSize: chOpts.Map.DefaultPageSize,
|
||||
MinPageSize: chOpts.Map.MinPageSize,
|
||||
MaxPageSize: chOpts.Map.MaxPageSize,
|
||||
LiveTransitionMaxPublicationLimit: chOpts.Map.LiveTransitionMaxPublicationLimit,
|
||||
SubscribeCatchUpTimeout: chOpts.Map.SubscribeCatchUpTimeout.ToDuration(),
|
||||
}
|
||||
}
|
||||
hasSharedPoll := appCfg.Channel.WithoutNamespace.SubscriptionType == "shared_poll"
|
||||
if !hasSharedPoll {
|
||||
for _, ns := range appCfg.Channel.Namespaces {
|
||||
if ns.SubscriptionType == "shared_poll" {
|
||||
hasSharedPoll = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasSharedPoll {
|
||||
cfg.SharedPoll.ConcurrencyLimit = appCfg.SharedPoll.ConcurrencyLimit
|
||||
cfg.SharedPoll.GetSharedPollChannelOptions = func(channel string) (centrifuge.SharedPollChannelOptions, bool) {
|
||||
_, _, chOpts, ok, err := cfgContainer.ChannelOptions(channel)
|
||||
if err != nil || !ok {
|
||||
return centrifuge.SharedPollChannelOptions{}, false
|
||||
}
|
||||
if chOpts.SubscriptionType != "shared_poll" {
|
||||
return centrifuge.SharedPollChannelOptions{}, false
|
||||
}
|
||||
return centrifuge.SharedPollChannelOptions{
|
||||
MaxKeysPerConnection: chOpts.SharedPoll.MaxKeysPerConnection,
|
||||
RefreshInterval: chOpts.SharedPoll.RefreshInterval.ToDuration(),
|
||||
RefreshBatchSize: chOpts.SharedPoll.RefreshBatchSize,
|
||||
Mode: chOpts.SharedPoll.Mode,
|
||||
ChannelShutdownDelay: chOpts.SharedPoll.ChannelShutdownDelay.ToDuration(),
|
||||
TrackExpiredExtraDelay: chOpts.SharedPoll.TrackExpiredExtraDelay.ToDuration(),
|
||||
PublishEnabled: chOpts.SharedPoll.PublishEnabled,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
cfg.HistoryMetaTTL = appCfg.Channel.HistoryMetaTTL.ToDuration()
|
||||
cfg.ClientConnectIncludeServerTime = appCfg.Client.ConnectIncludeServerTime
|
||||
cfg.LogLevel = logging.CentrifugeLogLevel(strings.ToLower(appCfg.Log.Level))
|
||||
cfg.LogHandler = logHandler
|
||||
if appCfg.Client.ConnectCodeToUnidirectionalDisconnect.Enabled {
|
||||
uniCodeTransforms := make(map[uint32]centrifuge.Disconnect)
|
||||
for _, transform := range appCfg.Client.ConnectCodeToUnidirectionalDisconnect.Transforms {
|
||||
uniCodeTransforms[transform.Code] = centrifuge.Disconnect{Code: transform.To.Code, Reason: transform.To.Reason}
|
||||
}
|
||||
cfg.UnidirectionalCodeToDisconnect = uniCodeTransforms
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// nodeName returns a name for this Centrifugo node. If no name provided
|
||||
// in configuration then it constructs node name based on hostname and port
|
||||
func nodeName(cfg config.Config) string {
|
||||
name := cfg.Node.Name
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
port := strconv.Itoa(cfg.HTTP.Port)
|
||||
var hostname string
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = "?"
|
||||
}
|
||||
return hostname + "_" + port
|
||||
}
|
||||
49
internal/app/origin.go
Normal file
49
internal/app/origin.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/origin"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var warnAllowedOriginsOnce sync.Once
|
||||
|
||||
func getCheckOrigin(cfg config.Config) func(r *http.Request) bool {
|
||||
allowedOrigins := cfg.Client.AllowedOrigins
|
||||
if len(allowedOrigins) == 0 {
|
||||
return func(r *http.Request) bool {
|
||||
// Only allow connections without Origin in this case.
|
||||
originHeader := r.Header.Get("Origin")
|
||||
if originHeader == "" {
|
||||
return true
|
||||
}
|
||||
log.Info().Str("origin", originHeader).Msg("request Origin is not authorized due to empty allowed_origins")
|
||||
return false
|
||||
}
|
||||
}
|
||||
originChecker, err := origin.NewPatternChecker(allowedOrigins)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating origin checker")
|
||||
}
|
||||
if len(allowedOrigins) == 1 && allowedOrigins[0] == "*" {
|
||||
// Fast path for *.
|
||||
warnAllowedOriginsOnce.Do(func() {
|
||||
log.Warn().Msg("usage of allowed_origins * is discouraged for security reasons, consider setting exact list of origins")
|
||||
})
|
||||
return func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return func(r *http.Request) bool {
|
||||
ok := originChecker.Check(r)
|
||||
if !ok {
|
||||
log.Info().Str("origin", r.Header.Get("Origin")).Strs("allowed_origins", allowedOrigins).Msg("request Origin is not authorized")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
533
internal/app/proxy.go
Normal file
533
internal/app/proxy.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/client"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/proxy"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/tools"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func buildProxyMap(cfg config.Config) (*client.ProxyMap, bool, error) {
|
||||
proxyMap := &client.ProxyMap{
|
||||
ConnectProxy: nil,
|
||||
RefreshProxy: nil,
|
||||
RpcProxies: map[string]proxy.RPCProxy{},
|
||||
PublishProxies: map[string]proxy.PublishProxy{},
|
||||
SubscribeProxies: map[string]proxy.SubscribeProxy{},
|
||||
SubRefreshProxies: map[string]proxy.SubRefreshProxy{},
|
||||
SubscribeStreamProxies: map[string]*proxy.SubscribeStreamProxy{},
|
||||
MapPublishProxies: map[string]proxy.MapPublishProxy{},
|
||||
MapRemoveProxies: map[string]proxy.MapRemoveProxy{},
|
||||
SharedPollRefreshProxies: map[string]*proxy.SharedPollRefreshHandler{},
|
||||
}
|
||||
|
||||
var keepHeadersInContext bool
|
||||
var keepEmulatedHeadersInContext bool
|
||||
|
||||
var err error
|
||||
var proxyFound bool
|
||||
|
||||
if cfg.Client.Proxy.Connect.Enabled {
|
||||
p := cfg.Client.Proxy.Connect
|
||||
proxyMap.ConnectProxy, err = proxy.GetConnectProxy("connect", cfg.Client.Proxy.Connect.Proxy)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating connect proxy: %w", err)
|
||||
}
|
||||
log.Info().Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("connect proxy enabled")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Client.Proxy.Refresh.Enabled {
|
||||
p := cfg.Client.Proxy.Refresh
|
||||
proxyMap.RefreshProxy, err = proxy.GetRefreshProxy("refresh", cfg.Client.Proxy.Refresh.Proxy)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating refresh proxy: %w", err)
|
||||
}
|
||||
log.Info().Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("refresh proxy enabled")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
namedProxies := make(map[string]configtypes.Proxy)
|
||||
for _, p := range cfg.Proxies {
|
||||
namedProxies[p.Name] = p.Proxy
|
||||
}
|
||||
|
||||
subscribeProxyEnabled := cfg.Channel.WithoutNamespace.SubscribeProxyEnabled
|
||||
subscribeProxyName := cfg.Channel.WithoutNamespace.SubscribeProxyName
|
||||
if subscribeProxyEnabled {
|
||||
var p proxy.Config
|
||||
if subscribeProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.Subscribe
|
||||
} else {
|
||||
p, proxyFound = namedProxies[subscribeProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("subscribe proxy not found: %s", subscribeProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.SubscribeProxies[subscribeProxyName]; !ok {
|
||||
sp, err := proxy.GetSubscribeProxy(subscribeProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating subscribe proxy %s: %w", subscribeProxyName, err)
|
||||
}
|
||||
proxyMap.SubscribeProxies[subscribeProxyName] = sp
|
||||
}
|
||||
log.Info().Str("proxy_name", subscribeProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("subscribe proxy enabled for channels without namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
publishProxyEnabled := cfg.Channel.WithoutNamespace.PublishProxyEnabled
|
||||
publishProxyName := cfg.Channel.WithoutNamespace.PublishProxyName
|
||||
if publishProxyEnabled {
|
||||
var p proxy.Config
|
||||
if publishProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.Publish
|
||||
} else {
|
||||
p, proxyFound = namedProxies[publishProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("publish proxy not found: %s", publishProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.PublishProxies[publishProxyName]; !ok {
|
||||
pp, err := proxy.GetPublishProxy(publishProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating publish proxy %s: %w", publishProxyName, err)
|
||||
}
|
||||
proxyMap.PublishProxies[publishProxyName] = pp
|
||||
}
|
||||
log.Info().Str("proxy_name", publishProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("publish proxy enabled for channels without namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
subRefreshProxyEnabled := cfg.Channel.WithoutNamespace.SubRefreshProxyEnabled
|
||||
subRefreshProxyName := cfg.Channel.WithoutNamespace.SubRefreshProxyName
|
||||
if subRefreshProxyEnabled {
|
||||
var p proxy.Config
|
||||
if subRefreshProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.SubRefresh
|
||||
} else {
|
||||
p, proxyFound = namedProxies[subRefreshProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("sub refresh proxy not found: %s", subRefreshProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.SubRefreshProxies[subRefreshProxyName]; !ok {
|
||||
srp, err := proxy.GetSubRefreshProxy(subRefreshProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating sub refresh proxy %s: %w", subRefreshProxyName, err)
|
||||
}
|
||||
proxyMap.SubRefreshProxies[subRefreshProxyName] = srp
|
||||
}
|
||||
log.Info().Str("proxy_name", subRefreshProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("sub refresh proxy enabled for channels without namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
subscribeStreamProxyEnabled := cfg.Channel.WithoutNamespace.SubscribeStreamProxyEnabled
|
||||
subscribeStreamProxyName := cfg.Channel.WithoutNamespace.SubscribeStreamProxyName
|
||||
if subscribeStreamProxyEnabled {
|
||||
var p proxy.Config
|
||||
if subscribeStreamProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.SubscribeStream
|
||||
} else {
|
||||
p, proxyFound = namedProxies[subscribeStreamProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("subscribe stream proxy not found: %s", subscribeStreamProxyName)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(p.Endpoint, "http") {
|
||||
log.Fatal().Str("name", subscribeStreamProxyName).Msg("error creating subscribe stream proxy – only GRPC endpoints supported")
|
||||
}
|
||||
if _, ok := proxyMap.SubscribeStreamProxies[subscribeStreamProxyName]; !ok {
|
||||
sp, err := proxy.NewSubscribeStreamProxy(subscribeStreamProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating subscribe stream proxy %s: %w", subscribeStreamProxyName, err)
|
||||
}
|
||||
proxyMap.SubscribeStreamProxies[subscribeStreamProxyName] = sp
|
||||
}
|
||||
log.Info().Str("proxy_name", subscribeStreamProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("subscribe stream proxy enabled for channels without namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
mapPublishProxyEnabled := cfg.Channel.WithoutNamespace.Map.PublishProxyEnabled
|
||||
mapPublishProxyName := cfg.Channel.WithoutNamespace.Map.PublishProxyName
|
||||
if mapPublishProxyEnabled {
|
||||
var p proxy.Config
|
||||
if mapPublishProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.MapPublish
|
||||
} else {
|
||||
p, proxyFound = namedProxies[mapPublishProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("map publish proxy not found: %s", mapPublishProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.MapPublishProxies[mapPublishProxyName]; !ok {
|
||||
mpp, err := proxy.GetMapPublishProxy(mapPublishProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating map publish proxy %s: %w", mapPublishProxyName, err)
|
||||
}
|
||||
proxyMap.MapPublishProxies[mapPublishProxyName] = mpp
|
||||
}
|
||||
log.Info().Str("proxy_name", mapPublishProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("map publish proxy enabled for channels without namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
mapRemoveProxyEnabled := cfg.Channel.WithoutNamespace.Map.RemoveProxyEnabled
|
||||
mapRemoveProxyName := cfg.Channel.WithoutNamespace.Map.RemoveProxyName
|
||||
if mapRemoveProxyEnabled {
|
||||
var p proxy.Config
|
||||
if mapRemoveProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.MapRemove
|
||||
} else {
|
||||
p, proxyFound = namedProxies[mapRemoveProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("map remove proxy not found: %s", mapRemoveProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.MapRemoveProxies[mapRemoveProxyName]; !ok {
|
||||
mrp, err := proxy.GetMapRemoveProxy(mapRemoveProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating map remove proxy %s: %w", mapRemoveProxyName, err)
|
||||
}
|
||||
proxyMap.MapRemoveProxies[mapRemoveProxyName] = mrp
|
||||
}
|
||||
log.Info().Str("proxy_name", mapRemoveProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("map remove proxy enabled for channels without namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ns := range cfg.Channel.Namespaces {
|
||||
subscribeProxyEnabled := ns.SubscribeProxyEnabled
|
||||
subscribeProxyName := ns.SubscribeProxyName
|
||||
if subscribeProxyEnabled {
|
||||
var p proxy.Config
|
||||
if subscribeProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.Subscribe
|
||||
} else {
|
||||
p, proxyFound = namedProxies[subscribeProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("subscribe proxy not found: %s", subscribeProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.SubscribeProxies[subscribeProxyName]; !ok {
|
||||
sp, err := proxy.GetSubscribeProxy(subscribeProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating subscribe proxy %s: %w", subscribeProxyName, err)
|
||||
}
|
||||
proxyMap.SubscribeProxies[subscribeProxyName] = sp
|
||||
}
|
||||
log.Info().Str("proxy_name", subscribeProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("subscribe proxy enabled for channels in namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
publishProxyEnabled := ns.PublishProxyEnabled
|
||||
publishProxyName := ns.PublishProxyName
|
||||
if publishProxyEnabled {
|
||||
var p proxy.Config
|
||||
if publishProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.Publish
|
||||
} else {
|
||||
p, proxyFound = namedProxies[publishProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("publish proxy not found: %s", publishProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.PublishProxies[publishProxyName]; !ok {
|
||||
pp, err := proxy.GetPublishProxy(publishProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating publish proxy %s: %w", publishProxyName, err)
|
||||
}
|
||||
proxyMap.PublishProxies[publishProxyName] = pp
|
||||
}
|
||||
log.Info().Str("proxy_name", publishProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("publish proxy enabled for channels in namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
subRefreshProxyEnabled := ns.SubRefreshProxyEnabled
|
||||
subRefreshProxyName := ns.SubRefreshProxyName
|
||||
if subRefreshProxyEnabled {
|
||||
var p proxy.Config
|
||||
if subRefreshProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.SubRefresh
|
||||
} else {
|
||||
p, proxyFound = namedProxies[subRefreshProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("sub refresh proxy not found: %s", subRefreshProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.SubRefreshProxies[subRefreshProxyName]; !ok {
|
||||
srp, err := proxy.GetSubRefreshProxy(subRefreshProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating sub refresh proxy %s: %w", subRefreshProxyName, err)
|
||||
}
|
||||
proxyMap.SubRefreshProxies[subRefreshProxyName] = srp
|
||||
}
|
||||
log.Info().Str("proxy_name", subRefreshProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("sub refresh proxy enabled for channels in namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
subscribeStreamProxyEnabled := ns.SubscribeStreamProxyEnabled
|
||||
subscribeStreamProxyName := ns.SubscribeStreamProxyName
|
||||
if subscribeStreamProxyEnabled {
|
||||
var p proxy.Config
|
||||
if subscribeStreamProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.SubscribeStream
|
||||
} else {
|
||||
p, proxyFound = namedProxies[subscribeStreamProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("subscribe stream proxy not found: %s", subscribeStreamProxyName)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(p.Endpoint, "http") {
|
||||
return nil, false, fmt.Errorf("error creating subscribe stream proxy %s only GRPC endpoints supported", subscribeStreamProxyName)
|
||||
}
|
||||
if _, ok := proxyMap.SubscribeStreamProxies[subscribeStreamProxyName]; !ok {
|
||||
sp, err := proxy.NewSubscribeStreamProxy(subscribeStreamProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating subscribe stream proxy %s: %w", subscribeStreamProxyName, err)
|
||||
}
|
||||
proxyMap.SubscribeStreamProxies[subscribeStreamProxyName] = sp
|
||||
}
|
||||
log.Info().Str("proxy_name", subscribeStreamProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("subscribe stream proxy enabled for channels in namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
mapPublishProxyEnabled := ns.Map.PublishProxyEnabled
|
||||
mapPublishProxyName := ns.Map.PublishProxyName
|
||||
if mapPublishProxyEnabled {
|
||||
var p proxy.Config
|
||||
if mapPublishProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.MapPublish
|
||||
} else {
|
||||
p, proxyFound = namedProxies[mapPublishProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("map publish proxy not found: %s", mapPublishProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.MapPublishProxies[mapPublishProxyName]; !ok {
|
||||
mpp, err := proxy.GetMapPublishProxy(mapPublishProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating map publish proxy %s: %w", mapPublishProxyName, err)
|
||||
}
|
||||
proxyMap.MapPublishProxies[mapPublishProxyName] = mpp
|
||||
}
|
||||
log.Info().Str("proxy_name", mapPublishProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("map publish proxy enabled for channels in namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
mapRemoveProxyEnabled := ns.Map.RemoveProxyEnabled
|
||||
mapRemoveProxyName := ns.Map.RemoveProxyName
|
||||
if mapRemoveProxyEnabled {
|
||||
var p proxy.Config
|
||||
if mapRemoveProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.MapRemove
|
||||
} else {
|
||||
p, proxyFound = namedProxies[mapRemoveProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("map remove proxy not found: %s", mapRemoveProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.MapRemoveProxies[mapRemoveProxyName]; !ok {
|
||||
mrp, err := proxy.GetMapRemoveProxy(mapRemoveProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating map remove proxy %s: %w", mapRemoveProxyName, err)
|
||||
}
|
||||
proxyMap.MapRemoveProxies[mapRemoveProxyName] = mrp
|
||||
}
|
||||
log.Info().Str("proxy_name", mapRemoveProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("map remove proxy enabled for channels in namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
if ns.SubscriptionType == "shared_poll" {
|
||||
sharedPollProxyName := ns.SharedPoll.ProxyName
|
||||
if sharedPollProxyName == "" {
|
||||
sharedPollProxyName = config.DefaultProxyName
|
||||
}
|
||||
var p proxy.Config
|
||||
if sharedPollProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.SharedPollRefresh
|
||||
} else {
|
||||
p, proxyFound = namedProxies[sharedPollProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("shared poll refresh proxy not found: %s", sharedPollProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.SharedPollRefreshProxies[sharedPollProxyName]; !ok {
|
||||
sp, err := proxy.GetSharedPollRefreshProxy(sharedPollProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating shared poll refresh proxy %s: %w", sharedPollProxyName, err)
|
||||
}
|
||||
handler := proxy.NewSharedPollRefreshHandler(proxy.SharedPollRefreshHandlerConfig{
|
||||
Proxy: sp,
|
||||
Name: sharedPollProxyName,
|
||||
})
|
||||
proxyMap.SharedPollRefreshProxies[sharedPollProxyName] = handler
|
||||
}
|
||||
log.Info().Str("proxy_name", sharedPollProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("shared poll refresh proxy enabled for channels in namespace")
|
||||
}
|
||||
}
|
||||
|
||||
// Also check without-namespace channels for shared poll proxy.
|
||||
if cfg.Channel.WithoutNamespace.SubscriptionType == "shared_poll" {
|
||||
sharedPollProxyName := cfg.Channel.WithoutNamespace.SharedPoll.ProxyName
|
||||
if sharedPollProxyName == "" {
|
||||
sharedPollProxyName = config.DefaultProxyName
|
||||
}
|
||||
var p proxy.Config
|
||||
if sharedPollProxyName == config.DefaultProxyName {
|
||||
p = cfg.Channel.Proxy.SharedPollRefresh
|
||||
} else {
|
||||
p, proxyFound = namedProxies[sharedPollProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("shared poll refresh proxy not found: %s", sharedPollProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.SharedPollRefreshProxies[sharedPollProxyName]; !ok {
|
||||
sp, err := proxy.GetSharedPollRefreshProxy(sharedPollProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating shared poll refresh proxy %s: %w", sharedPollProxyName, err)
|
||||
}
|
||||
handler := proxy.NewSharedPollRefreshHandler(proxy.SharedPollRefreshHandlerConfig{
|
||||
Proxy: sp,
|
||||
Name: sharedPollProxyName,
|
||||
})
|
||||
proxyMap.SharedPollRefreshProxies[sharedPollProxyName] = handler
|
||||
}
|
||||
log.Info().Str("proxy_name", sharedPollProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("shared poll refresh proxy enabled for channels without namespace")
|
||||
}
|
||||
|
||||
rpcProxyEnabled := cfg.RPC.WithoutNamespace.ProxyEnabled
|
||||
rpcProxyName := cfg.RPC.WithoutNamespace.ProxyName
|
||||
if rpcProxyEnabled {
|
||||
var p proxy.Config
|
||||
if rpcProxyName == config.DefaultProxyName {
|
||||
p = cfg.RPC.Proxy
|
||||
} else {
|
||||
p, proxyFound = namedProxies[rpcProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("rpc proxy not found: %s", rpcProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.RpcProxies[rpcProxyName]; !ok {
|
||||
rp, err := proxy.GetRpcProxy(rpcProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating rpc proxy %s: %w", rpcProxyName, err)
|
||||
}
|
||||
proxyMap.RpcProxies[rpcProxyName] = rp
|
||||
}
|
||||
log.Info().Str("proxy_name", rpcProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Msg("RPC proxy enabled for methods without namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ns := range cfg.RPC.Namespaces {
|
||||
rpcProxyEnabled := ns.ProxyEnabled
|
||||
rpcProxyName := ns.ProxyName
|
||||
if rpcProxyEnabled {
|
||||
var p proxy.Config
|
||||
if rpcProxyName == config.DefaultProxyName {
|
||||
p = cfg.RPC.Proxy
|
||||
} else {
|
||||
p, proxyFound = namedProxies[rpcProxyName]
|
||||
if !proxyFound {
|
||||
return nil, false, fmt.Errorf("rpc proxy not found: %s", rpcProxyName)
|
||||
}
|
||||
}
|
||||
if _, ok := proxyMap.RpcProxies[rpcProxyName]; !ok {
|
||||
rp, err := proxy.GetRpcProxy(rpcProxyName, p)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error creating rpc proxy %s: %w", rpcProxyName, err)
|
||||
}
|
||||
proxyMap.RpcProxies[rpcProxyName] = rp
|
||||
}
|
||||
log.Info().Str("proxy_name", rpcProxyName).Str("endpoint", tools.RedactedLogURLs(p.Endpoint)[0]).Str("namespace", ns.Name).Msg("RPC proxy enabled for namespace")
|
||||
if len(p.HttpHeaders) > 0 {
|
||||
keepHeadersInContext = true
|
||||
}
|
||||
if len(p.EmulatedHeaders) > 0 {
|
||||
keepEmulatedHeadersInContext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxyMap.UsesEmulatedHeaders = keepEmulatedHeadersInContext
|
||||
|
||||
return proxyMap, keepHeadersInContext, nil
|
||||
}
|
||||
445
internal/app/run.go
Normal file
445
internal/app/run.go
Normal file
@@ -0,0 +1,445 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/api"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/build"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/client"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/confighelpers"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/consuming"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/jwtverify"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/logging"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/metrics"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/notify"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/service"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/survey"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/telemetry"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/tools"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/usage"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
_ "google.golang.org/grpc/encoding/gzip"
|
||||
)
|
||||
|
||||
const edition = "oss"
|
||||
|
||||
func Run(cmd *cobra.Command, configFile string) {
|
||||
dotEnvUsed := false
|
||||
if tools.FileExists(".env") {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error loading .env file")
|
||||
}
|
||||
dotEnvUsed = true
|
||||
}
|
||||
cfg, cfgMeta, err := config.GetConfig(cmd, configFile)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error getting config")
|
||||
}
|
||||
|
||||
ctx, serviceCancel := context.WithCancel(context.Background())
|
||||
defer serviceCancel()
|
||||
|
||||
// Ingest services (async consumers) have a separate lifecycle from the rest of
|
||||
// services – see the shutdown sequence in handleSignals.
|
||||
ingestCtx, ingestCancel := context.WithCancel(context.Background())
|
||||
defer ingestCancel()
|
||||
|
||||
centrifugeLogHandler, logCloseFn := logging.Setup(cfg)
|
||||
if logCloseFn != nil {
|
||||
defer logCloseFn()
|
||||
}
|
||||
if cfgMeta.FileNotFound {
|
||||
log.Warn().Msg("config file not found, continue using environment and flag options")
|
||||
} else {
|
||||
absConfPath, _ := filepath.Abs(configFile)
|
||||
log.Info().Str("path", absConfPath).Msg("using config file")
|
||||
if dotEnvUsed {
|
||||
log.Info().Msg("environment variables have been loaded from .env file")
|
||||
}
|
||||
}
|
||||
err = tools.WritePidFile(cfg.PidFile)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error writing PID")
|
||||
}
|
||||
_, _ = maxprocs.Set(maxprocs.Logger(func(s string, i ...interface{}) {
|
||||
log.Info().Msgf(strings.ToLower(s), i...)
|
||||
}))
|
||||
|
||||
// Registered services will be run after node.Run() but before HTTP/GRPC servers start.
|
||||
// Registered services will be stopped after node's shutdown and HTTP/GRPC servers shutdown.
|
||||
serviceManager := service.NewManager()
|
||||
|
||||
entry := log.Info().
|
||||
Str("version", build.Version).
|
||||
Str("runtime", runtime.Version()).
|
||||
Int("pid", os.Getpid()).
|
||||
Int("gomaxprocs", runtime.GOMAXPROCS(0))
|
||||
|
||||
if fips140.Enabled() { // See https://go.dev/doc/security/fips140.
|
||||
entry = entry.Bool("fips", true)
|
||||
}
|
||||
|
||||
if cfg.Broker.Enabled {
|
||||
entry = entry.Str("broker", cfg.Broker.Type)
|
||||
}
|
||||
if cfg.PresenceManager.Enabled {
|
||||
entry = entry.Str("presence_manager", cfg.PresenceManager.Type)
|
||||
}
|
||||
if !cfg.Broker.Enabled || !cfg.PresenceManager.Enabled {
|
||||
entry = entry.Str("engine", cfg.Engine.Type)
|
||||
}
|
||||
entry.Msg("starting Centrifugo")
|
||||
|
||||
if build.Version == "0.0.0" {
|
||||
log.Warn().Msg("running a development build of Centrifugo (version 0.0.0), ensure to use release build in production")
|
||||
}
|
||||
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error validating config")
|
||||
}
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating config")
|
||||
}
|
||||
cfgContainer.ChannelOptionsCacheTTL = 200 * time.Millisecond
|
||||
|
||||
// Initialize centralized metrics registry.
|
||||
err = metrics.Init(metrics.Config{
|
||||
Namespace: "", // Use default "centrifugo" namespace.
|
||||
ConstLabels: nil, // Can be populated from config in the future.
|
||||
Registerer: nil, // Use prometheus.DefaultRegisterer.
|
||||
NativeHistograms: cfg.Prometheus.NativeHistograms,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error initializing metrics")
|
||||
}
|
||||
|
||||
proxyMap, keepHeadersInContext, err := buildProxyMap(cfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error building proxy map")
|
||||
}
|
||||
|
||||
nodeCfg := centrifugeNodeConfig(build.Version, edition, cfgContainer, centrifugeLogHandler)
|
||||
node, err := centrifuge.New(nodeCfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating Centrifuge Node")
|
||||
}
|
||||
|
||||
if cfg.OpenTelemetry.Enabled {
|
||||
_, err := telemetry.SetupTracing(context.Background(), cfg.OpenTelemetry, node.ID())
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error setting up opentelemetry tracing")
|
||||
}
|
||||
}
|
||||
|
||||
err = configureEngines(node, cfgContainer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("configure engines error")
|
||||
}
|
||||
|
||||
err = configureMapBroker(node, cfgContainer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("configure map broker error")
|
||||
}
|
||||
|
||||
verifierConfig, err := confighelpers.MakeVerifierConfig(cfg.Client.Token)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating JWT verifier config")
|
||||
}
|
||||
|
||||
tokenVerifier, err := jwtverify.NewTokenVerifierJWT(verifierConfig, cfgContainer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating token verifier")
|
||||
}
|
||||
|
||||
var subTokenVerifier *jwtverify.VerifierJWT
|
||||
if cfg.Client.SubscriptionToken.Enabled {
|
||||
log.Info().Msg("initializing separate verifier for subscription tokens")
|
||||
subVerifier, err := confighelpers.MakeVerifierConfig(cfg.Client.SubscriptionToken.Token)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating subscription JWT verifier config")
|
||||
}
|
||||
subTokenVerifier, err = jwtverify.NewTokenVerifierJWT(subVerifier, cfgContainer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating token verifier")
|
||||
}
|
||||
}
|
||||
|
||||
clientHandler := client.NewHandler(node, cfgContainer, tokenVerifier, subTokenVerifier, proxyMap)
|
||||
err = clientHandler.Setup()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error setting up client handler")
|
||||
}
|
||||
if cfg.RPC.Ping.Enabled {
|
||||
log.Info().Str("method", cfg.RPC.Ping.Method).Msg("RPC ping extension enabled")
|
||||
clientHandler.SetRPCExtension(cfg.RPC.Ping.Method, func(c client.Client, e centrifuge.RPCEvent) (centrifuge.RPCReply, error) {
|
||||
return centrifuge.RPCReply{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
surveyCaller := survey.NewCaller(node)
|
||||
|
||||
useAPIOpentelemetry := cfg.OpenTelemetry.Enabled && cfg.OpenTelemetry.API
|
||||
useConsumingOpentelemetry := cfg.OpenTelemetry.Enabled && cfg.OpenTelemetry.Consuming
|
||||
|
||||
httpAPIExecutor := api.NewExecutor(node, cfgContainer, surveyCaller, api.ExecutorConfig{
|
||||
Protocol: "http",
|
||||
UseOpenTelemetry: useAPIOpentelemetry,
|
||||
})
|
||||
grpcAPIExecutor := api.NewExecutor(node, cfgContainer, surveyCaller, api.ExecutorConfig{
|
||||
Protocol: "grpc",
|
||||
UseOpenTelemetry: useAPIOpentelemetry,
|
||||
})
|
||||
consumingAPIExecutor := api.NewExecutor(node, cfgContainer, surveyCaller, api.ExecutorConfig{
|
||||
Protocol: "consuming",
|
||||
UseOpenTelemetry: useConsumingOpentelemetry,
|
||||
})
|
||||
|
||||
consumingHandler := api.NewConsumingHandler(node, consumingAPIExecutor, api.ConsumingHandlerConfig{
|
||||
UseOpenTelemetry: useConsumingOpentelemetry,
|
||||
})
|
||||
|
||||
consumingServices, err := consuming.New(node.ID(), consumingHandler, cfg.Consumers)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error initializing consumers")
|
||||
}
|
||||
|
||||
// Consumers are registered in a separate manager: unlike other services they
|
||||
// must be stopped before Node shutdown, so that no consumed message is
|
||||
// acknowledged after this node lost the ability to deliver it.
|
||||
ingestManager := service.NewManager()
|
||||
ingestManager.Register(consumingServices...)
|
||||
|
||||
if cfg.Graphite.Enabled {
|
||||
serviceManager.Register(graphiteExporter(cfg, nodeCfg))
|
||||
}
|
||||
|
||||
var statsSender *usage.Sender
|
||||
if !cfg.UsageStats.Disabled {
|
||||
statsSender = usage.NewSender(node, cfgContainer, usage.Features{
|
||||
Edition: edition,
|
||||
Version: build.Version,
|
||||
EngineEnabled: !cfg.Broker.Enabled || !cfg.PresenceManager.Enabled,
|
||||
EngineType: cfg.Engine.Type,
|
||||
BrokerEnabled: cfg.Broker.Enabled,
|
||||
BrokerType: cfg.Broker.Type,
|
||||
PresenceManagerEnabled: cfg.PresenceManager.Enabled,
|
||||
PresenceManagerType: cfg.PresenceManager.Type,
|
||||
|
||||
Websocket: !cfg.WebSocket.Disabled,
|
||||
HTTPStream: cfg.HTTPStream.Enabled,
|
||||
SSE: cfg.SSE.Enabled,
|
||||
UniWebsocket: cfg.UniWS.Enabled,
|
||||
UniHTTPStream: cfg.UniHTTPStream.Enabled,
|
||||
UniSSE: cfg.UniSSE.Enabled,
|
||||
UniGRPC: cfg.UniGRPC.Enabled,
|
||||
WebTransport: cfg.WebTransport.Enabled,
|
||||
|
||||
EnabledConsumers: usage.GetEnabledConsumers(cfg.Consumers),
|
||||
|
||||
GrpcAPI: cfg.GrpcAPI.Enabled,
|
||||
SubscribeToPersonal: cfg.Client.SubscribeToUserPersonalChannel.Enabled,
|
||||
Admin: cfg.Admin.Enabled,
|
||||
|
||||
ConnectProxy: proxyMap.ConnectProxy != nil,
|
||||
RefreshProxy: proxyMap.RefreshProxy != nil,
|
||||
SubscribeProxy: len(proxyMap.SubscribeProxies) > 0,
|
||||
PublishProxy: len(proxyMap.PublishProxies) > 0,
|
||||
RPCProxy: len(proxyMap.RpcProxies) > 0,
|
||||
SubRefreshProxy: len(proxyMap.SubRefreshProxies) > 0,
|
||||
SubscribeStreamProxy: len(proxyMap.SubscribeStreamProxies) > 0,
|
||||
Swagger: cfg.Swagger.Enabled,
|
||||
})
|
||||
serviceManager.Register(statsSender)
|
||||
}
|
||||
|
||||
notify.RegisterHandlers(node, statsSender)
|
||||
|
||||
if err = node.Run(); err != nil {
|
||||
log.Fatal().Err(err).Msg("error running node")
|
||||
}
|
||||
|
||||
serviceDone := runServices(ctx, serviceManager)
|
||||
ingestDone := runServices(ingestCtx, ingestManager)
|
||||
|
||||
var grpcAPIServer *grpc.Server
|
||||
if cfg.GrpcAPI.Enabled {
|
||||
var err error
|
||||
grpcAPIServer, err = runGRPCAPIServer(cfg, node, useAPIOpentelemetry, grpcAPIExecutor)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating GRPC API server")
|
||||
}
|
||||
}
|
||||
|
||||
var grpcUniServer *grpc.Server
|
||||
if cfg.UniGRPC.Enabled {
|
||||
var err error
|
||||
grpcUniServer, err = runGRPCUniServer(cfg, node)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating uni GRPC server")
|
||||
}
|
||||
}
|
||||
|
||||
httpServers, err := runHTTPServers(node, cfgContainer, httpAPIExecutor, keepHeadersInContext)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error running HTTP server")
|
||||
}
|
||||
|
||||
logStartWarnings(cfg, cfgMeta)
|
||||
|
||||
handleSignals(
|
||||
cmd, configFile, node, cfgContainer, tokenVerifier, subTokenVerifier,
|
||||
httpServers, grpcAPIServer, grpcUniServer,
|
||||
serviceDone, serviceCancel, ingestDone, ingestCancel,
|
||||
)
|
||||
}
|
||||
|
||||
// runServices runs all services registered in the manager and returns a channel
|
||||
// closed when all of them stopped.
|
||||
func runServices(ctx context.Context, manager *service.Manager) chan struct{} {
|
||||
done := make(chan struct{})
|
||||
manager.Run(ctx)
|
||||
go func() {
|
||||
defer close(done)
|
||||
err := manager.Wait()
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Error().Err(err).Msg("error running service")
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
return done
|
||||
}
|
||||
|
||||
func handleSignals(
|
||||
cmd *cobra.Command, configFile string, n *centrifuge.Node, cfgContainer *config.Container,
|
||||
tokenVerifier *jwtverify.VerifierJWT, subTokenVerifier *jwtverify.VerifierJWT, httpServers []*http.Server,
|
||||
grpcAPIServer *grpc.Server, grpcUniServer *grpc.Server, serviceDone chan struct{},
|
||||
serviceCancel context.CancelFunc, ingestDone chan struct{}, ingestCancel context.CancelFunc,
|
||||
) {
|
||||
cfg := cfgContainer.Config()
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, os.Interrupt, syscall.SIGTERM)
|
||||
for {
|
||||
sig := <-sigCh
|
||||
log.Info().Msgf("signal received: %v", sig)
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
// Reload application configuration on SIGHUP.
|
||||
// Note that Centrifugo can't reload config for everything – just best effort to reload what's possible.
|
||||
// We can now reload channel options and token verifiers.
|
||||
log.Info().Msg("reloading configuration")
|
||||
newCfg, _, err := config.GetConfig(cmd, configFile)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error reading config")
|
||||
continue
|
||||
}
|
||||
if err = newCfg.Validate(); err != nil {
|
||||
log.Error().Msgf("error validating config: %v", err)
|
||||
continue
|
||||
}
|
||||
verifierConfig, err := confighelpers.MakeVerifierConfig(newCfg.Client.Token)
|
||||
if err != nil {
|
||||
log.Error().Msgf("error reloading: %v", err)
|
||||
continue
|
||||
}
|
||||
if err = tokenVerifier.Reload(verifierConfig); err != nil {
|
||||
log.Error().Msgf("error reloading: %v", err)
|
||||
continue
|
||||
}
|
||||
if subTokenVerifier != nil {
|
||||
subVerifierConfig, err := confighelpers.MakeVerifierConfig(newCfg.Client.SubscriptionToken.Token)
|
||||
if err != nil {
|
||||
log.Error().Msgf("error reloading: %v", err)
|
||||
continue
|
||||
}
|
||||
if err := subTokenVerifier.Reload(subVerifierConfig); err != nil {
|
||||
log.Error().Msgf("error reloading: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err = cfgContainer.Reload(newCfg); err != nil {
|
||||
log.Error().Msgf("error reloading: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Info().Msg("configuration successfully reloaded")
|
||||
case syscall.SIGINT, os.Interrupt, syscall.SIGTERM:
|
||||
log.Info().Msg("shutting down ...")
|
||||
pidFile := cfg.PidFile
|
||||
shutdownTimeout := cfg.Shutdown.Timeout
|
||||
go time.AfterFunc(shutdownTimeout.ToDuration(), func() {
|
||||
if pidFile != "" {
|
||||
_ = os.Remove(pidFile)
|
||||
}
|
||||
log.Fatal().Msg("shutdown timeout reached")
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if grpcAPIServer != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
grpcAPIServer.GracefulStop()
|
||||
}()
|
||||
}
|
||||
|
||||
if grpcUniServer != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
grpcUniServer.GracefulStop()
|
||||
}()
|
||||
}
|
||||
|
||||
for _, srv := range httpServers {
|
||||
wg.Add(1)
|
||||
go func(srv *http.Server) {
|
||||
defer wg.Done()
|
||||
_ = srv.Shutdown(context.Background()) // We have a separate timeout goroutine.
|
||||
}(srv)
|
||||
}
|
||||
|
||||
// Stop consuming messages before Node shutdown and wait until consumers
|
||||
// stopped. Otherwise a message consumed here may be acknowledged while
|
||||
// this node is already unable to deliver it – such message must be left
|
||||
// unacknowledged instead, to be redelivered upon restart. Note that we
|
||||
// don't wait for HTTP/GRPC servers here: their shutdown only completes
|
||||
// after Node disconnected clients, since long-lived streaming requests
|
||||
// (SSE, HTTP-streaming, unidirectional GRPC) are in-flight until then.
|
||||
ingestCancel()
|
||||
<-ingestDone
|
||||
|
||||
_ = n.Shutdown(context.Background()) // We have a separate timeout goroutine.
|
||||
wg.Wait()
|
||||
|
||||
serviceCancel()
|
||||
<-serviceDone
|
||||
|
||||
if pidFile != "" {
|
||||
_ = os.Remove(pidFile)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
81
internal/app/tls.go
Normal file
81
internal/app/tls.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
stdlog "log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/acme"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
var startHTTPChallengeServerOnce sync.Once
|
||||
|
||||
func GetTLSConfig(cfg config.Config) (*tls.Config, error) {
|
||||
tlsEnabled := cfg.HTTP.TLS.Enabled
|
||||
tlsAutocertEnabled := cfg.HTTP.TLSAutocert.Enabled
|
||||
tlsAutocertHostWhitelist := cfg.HTTP.TLSAutocert.HostWhitelist
|
||||
tlsAutocertCacheDir := cfg.HTTP.TLSAutocert.CacheDir
|
||||
tlsAutocertEmail := cfg.HTTP.TLSAutocert.Email
|
||||
tlsAutocertServerName := cfg.HTTP.TLSAutocert.ServerName
|
||||
tlsAutocertHTTP := cfg.HTTP.TLSAutocert.HTTP
|
||||
tlsAutocertHTTPAddr := cfg.HTTP.TLSAutocert.HTTPAddr
|
||||
|
||||
if tlsAutocertEnabled {
|
||||
certManager := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Email: tlsAutocertEmail,
|
||||
}
|
||||
if tlsAutocertHostWhitelist != nil {
|
||||
certManager.HostPolicy = autocert.HostWhitelist(tlsAutocertHostWhitelist...)
|
||||
}
|
||||
if tlsAutocertCacheDir != "" {
|
||||
certManager.Cache = autocert.DirCache(tlsAutocertCacheDir)
|
||||
}
|
||||
|
||||
if tlsAutocertHTTP {
|
||||
startHTTPChallengeServerOnce.Do(func() {
|
||||
// getTLSConfig can be called several times.
|
||||
acmeHTTPServer := &http.Server{
|
||||
Handler: certManager.HTTPHandler(nil),
|
||||
Addr: tlsAutocertHTTPAddr,
|
||||
ErrorLog: stdlog.New(&httpErrorLogWriter{log.Logger}, "", 0),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
go func() {
|
||||
log.Info().Msgf("serving ACME http_01 challenge on %s", tlsAutocertHTTPAddr)
|
||||
if err := acmeHTTPServer.ListenAndServe(); err != nil {
|
||||
log.Fatal().Err(err).Msgf("can't create server on %s to serve acme http challenge", tlsAutocertHTTPAddr)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
// See https://github.com/centrifugal/centrifugo/issues/144#issuecomment-279393819
|
||||
if tlsAutocertServerName != "" && hello.ServerName == "" {
|
||||
hello.ServerName = tlsAutocertServerName
|
||||
}
|
||||
return certManager.GetCertificate(hello)
|
||||
},
|
||||
NextProtos: []string{
|
||||
"h2", "http/1.1", acme.ALPNProto,
|
||||
},
|
||||
}, nil
|
||||
|
||||
} else if tlsEnabled {
|
||||
// Autocert disabled - just try to use provided SSL cert and key files.
|
||||
return cfg.HTTP.TLS.ToGoTLSConfig("http_server")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user