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

169 lines
5.7 KiB
Go

package proxy
import (
"encoding/json"
"slices"
"strings"
"time"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifugo/v6/internal/tools"
"github.com/rs/zerolog/log"
)
type Config = configtypes.Proxy
// droppedEmulatedHeaderLogLimiter throttles the (rare) migration hint emitted
// when a client sends an emulated header that would have been forwarded before
// v6.9.0 (listed in http_headers) but is now dropped because it is not in
// emulated_headers. Single global limiter - at most one line per interval
// across all proxies.
var droppedEmulatedHeaderLogLimiter = tools.NewIntervalLimiter(time.Minute)
// logDroppedEmulatedHeadersHint emits a one-off INFO hint listing every emulated
// header dropped for this request. It is only called when at least one header was
// dropped, and the body runs at most once per limiter interval, so it is
// effectively free on the hot path. allowedHeaders/allowedEmulatedHeaders are the
// lowercased http_headers / emulated_headers allow lists.
func logDroppedEmulatedHeadersHint(emulatedHeaders map[string]string, allowedHeaders, allowedEmulatedHeaders []string) {
if !droppedEmulatedHeaderLogLimiter.Allow() {
return
}
dropped := droppedEmulatedHeaderNames(emulatedHeaders, allowedHeaders, allowedEmulatedHeaders)
if len(dropped) == 0 {
return
}
// Informational only - deliberately no call to action. These names came from
// the client, so this may be a missed migration OR an unexpected/malicious
// client probing http_headers names; the operator must decide. Never imply the
// names should be allow-listed: their values are client-controlled.
log.Info().Strs("headers", dropped).Msg("client sent emulation headers matching http_headers but not emulated_headers - not forwarded to proxy (since v6.9.0); values are client-controlled, see headers emulation docs")
}
// droppedEmulatedHeaderNames returns the lowercased names of client-supplied
// emulated headers that are listed in http_headers but not emulated_headers
// - i.e. names that would have been forwarded before v6.9.0 and are now dropped.
func droppedEmulatedHeaderNames(emulatedHeaders map[string]string, allowedHeaders, allowedEmulatedHeaders []string) []string {
var dropped []string
for k := range emulatedHeaders {
lk := strings.ToLower(k)
if !slices.Contains(allowedEmulatedHeaders, lk) && slices.Contains(allowedHeaders, lk) {
dropped = append(dropped, lk)
}
}
return dropped
}
// normalizeProxyHeaderNames lowercases configured header/metadata name allow
// lists so matching against incoming header names is case-insensitive. It must
// be called for every proxy before use.
func normalizeProxyHeaderNames(p *Config) {
for i, header := range p.HttpHeaders {
p.HttpHeaders[i] = strings.ToLower(header)
}
for i, header := range p.EmulatedHeaders {
p.EmulatedHeaders[i] = strings.ToLower(header)
}
}
// emulatedHeaderAllowList returns the set of header names that may be sourced
// from the client headers emulation map (the client connect frame) - i.e. the
// emulated_headers allow list. The http_headers allow list is reserved for
// transport-level headers and is never sourced from emulation.
//
// A header name may legitimately appear in both http_headers and emulated_headers
// (e.g. an Authorization token a browser sends via emulation and a native client
// sends as a real transport header): the transport value is preferred and the
// emulated value is used as a fallback, so this overlap is not flagged.
func emulatedHeaderAllowList(p Config) []string {
return p.EmulatedHeaders
}
func getEncoding(useBase64 bool) string {
if useBase64 {
return "binary"
}
return "json"
}
func isHttpEndpoint(endpoint string) bool {
return strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://")
}
func GetConnectProxy(name string, p Config) (ConnectProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPConnectProxy(p)
}
return NewGRPCConnectProxy(name, p)
}
func GetRefreshProxy(name string, p Config) (RefreshProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPRefreshProxy(p)
}
return NewGRPCRefreshProxy(name, p)
}
func GetRpcProxy(name string, p Config) (RPCProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPRPCProxy(p)
}
return NewGRPCRPCProxy(name, p)
}
func GetSubRefreshProxy(name string, p Config) (SubRefreshProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPSubRefreshProxy(p)
}
return NewGRPCSubRefreshProxy(name, p)
}
func GetPublishProxy(name string, p Config) (PublishProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPPublishProxy(p)
}
return NewGRPCPublishProxy(name, p)
}
func GetSubscribeProxy(name string, p Config) (SubscribeProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPSubscribeProxy(p)
}
return NewGRPCSubscribeProxy(name, p)
}
func GetMapPublishProxy(name string, p Config) (MapPublishProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPMapPublishProxy(p)
}
return NewGRPCMapPublishProxy(name, p)
}
func GetMapRemoveProxy(name string, p Config) (MapRemoveProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPMapRemoveProxy(p)
}
return NewGRPCMapRemoveProxy(name, p)
}
func GetSharedPollRefreshProxy(name string, p Config) (SharedPollRefreshProxy, error) {
normalizeProxyHeaderNames(&p)
if isHttpEndpoint(p.Endpoint) {
return NewHTTPSharedPollRefreshProxy(p)
}
return NewGRPCSharedPollRefreshProxy(name, p)
}
type PerCallData struct {
Meta json.RawMessage
}