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

This commit is contained in:
upstream-mirror
2026-09-16 01:17:21 +05:00
commit b352a9a30d
472 changed files with 108375 additions and 0 deletions

21
internal/app/cmd.go Normal file
View 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
View 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 &centrifuge.MemoryBrokerConfig{}, nil
}
func memoryPresenceManagerConfig() (*centrifuge.MemoryPresenceManagerConfig, error) {
return &centrifuge.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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}