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

182 lines
5.1 KiB
Go

package proxy
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"slices"
"strings"
"github.com/centrifugal/centrifugo/v6/internal/clientcontext"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifugo/v6/internal/middleware"
"github.com/centrifugal/centrifugo/v6/internal/proxyproto"
"google.golang.org/grpc/metadata"
)
type baseRequestHTTP struct {
ClientID string `json:"client"`
Transport string `json:"transport"`
Protocol string `json:"protocol"`
Encoding string `json:"encoding"`
}
var httpEncoder = &proxyproto.JSONEncoder{}
var httpDecoder = &proxyproto.JSONDecoder{}
// DefaultMaxIdleConnsPerHost is a reasonable value for all HTTP clients.
const DefaultMaxIdleConnsPerHost = 255
// HTTPCaller is responsible for calling HTTP.
type HTTPCaller interface {
CallHTTP(context.Context, string, http.Header, []byte) ([]byte, error)
}
type httpCaller struct {
Endpoint string
HTTPClient *http.Client
}
// NewHTTPCaller creates new HTTPCaller.
func NewHTTPCaller(httpClient *http.Client) HTTPCaller {
return &httpCaller{
HTTPClient: httpClient,
}
}
func proxyHTTPClient(p configtypes.Proxy, logTraceEntity string) (*http.Client, error) {
var tlsConfig *tls.Config
if p.HTTP.TLS.Enabled {
var err error
tlsConfig, err = p.HTTP.TLS.ToGoTLSConfig(logTraceEntity)
if err != nil {
return nil, fmt.Errorf("error creating TLS config: %w", err)
}
}
return &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: DefaultMaxIdleConnsPerHost,
TLSClientConfig: tlsConfig,
},
Timeout: p.Timeout.ToDuration(),
}, nil
}
type statusCodeError struct {
Code int
}
func (e *statusCodeError) Error() string {
return fmt.Sprintf("unexpected HTTP status code: %d", e.Code)
}
func (c *httpCaller) CallHTTP(ctx context.Context, endpoint string, header http.Header, reqData []byte) ([]byte, error) {
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(reqData))
if err != nil {
return nil, fmt.Errorf("error constructing HTTP request: %w", err)
}
req.Header = header
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("HTTP request error: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, &statusCodeError{resp.StatusCode}
}
respData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading HTTP body: %w", err)
}
return respData, nil
}
func transformHTTPStatusError(err error, transforms []configtypes.HttpStatusToCodeTransform) (*proxyproto.Error, *proxyproto.Disconnect) {
if len(transforms) == 0 {
return nil, nil
}
var statusErr *statusCodeError
if !errors.As(err, &statusErr) {
return nil, nil
}
for _, t := range transforms {
if t.StatusCode == statusErr.Code {
if t.ToError.Code > 0 {
return &proxyproto.Error{
Code: t.ToError.Code,
Message: t.ToError.Message,
Temporary: t.ToError.Temporary,
}, nil
}
if t.ToDisconnect.Code > 0 {
return nil, &proxyproto.Disconnect{
Code: t.ToDisconnect.Code,
Reason: t.ToDisconnect.Reason,
}
}
}
}
return nil, nil
}
func httpRequestHeaders(ctx context.Context, proxy Config) http.Header {
return requestHeaders(ctx, proxy.HttpHeaders, proxy.GrpcMetadata, emulatedHeaderAllowList(proxy), proxy.HTTP.StaticHeaders)
}
func requestHeaders(ctx context.Context, allowedHeaders, allowedMetaKeys, allowedEmulatedHeaders []string, staticHeaders map[string]string) http.Header {
headers := http.Header{}
// Set static headers first, so that dynamic headers can override them.
for k, v := range staticHeaders {
headers.Set(k, v)
}
// Emulated headers come from the client connect frame and are client-controlled,
// so they are forwarded only when listed in emulated_headers - never via
// the transport-level http_headers allow list.
emulatedHeaders, _ := clientcontext.GetEmulatedHeadersFromContext(ctx)
var droppedEmulated bool
for k, v := range emulatedHeaders {
lk := strings.ToLower(k)
if slices.Contains(allowedEmulatedHeaders, lk) {
headers.Set(k, v)
} else if slices.Contains(allowedHeaders, lk) {
// Listed in http_headers but not emulated_headers: this would
// have been forwarded before v6.9.0. Flag for a throttled migration hint.
droppedEmulated = true
}
}
if droppedEmulated {
logDroppedEmulatedHeadersHint(emulatedHeaders, allowedHeaders, allowedEmulatedHeaders)
}
httpHeaders, hasHTTPHeaders := middleware.GetHeadersFromContext(ctx)
for k, vv := range httpHeaders {
if slices.Contains(allowedHeaders, strings.ToLower(k)) {
headers[k] = vv
}
}
if !hasHTTPHeaders {
md, _ := metadata.FromIncomingContext(ctx)
for k, vv := range md {
if slices.Contains(allowedMetaKeys, k) {
// Canonicalize the lowercase metadata key so the transport value
// lands on the same map key as emulated values written via Set
// and overrides them - otherwise both would be sent as duplicate
// headers with different casings.
headers[http.CanonicalHeaderKey(k)] = vv
}
}
}
headers.Set("Content-Type", "application/json")
return headers
}