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

239
internal/uniws/handler.go Normal file
View File

@@ -0,0 +1,239 @@
package uniws
import (
"net/http"
"sync"
"time"
"github.com/centrifugal/centrifugo/v6/internal/convert"
"github.com/centrifugal/centrifugo/v6/internal/logging"
"github.com/centrifugal/centrifugo/v6/internal/websocket"
"github.com/centrifugal/centrifuge"
"github.com/centrifugal/protocol"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
// Handler handles WebSocket client connections. Usually WebSocket protocol
// is a bidirectional connection between a client and a server for low-latency
// communication. Here we utilize only one direction - giving users an additional
// option for unidirectional transport.
type Handler struct {
node *centrifuge.Node
upgrade *websocket.Upgrader
config Config
pingPong centrifuge.PingPongConfig
}
// Support passing connect request in URL params.
// The value should be a properly encoded JSON object representing protocol.ConnectRequest.
const connectUrlParam = "cf_connect"
var writeBufferPool = &sync.Pool{}
// NewHandler creates new Handler.
func NewHandler(
n *centrifuge.Node, c Config, CheckOrigin func(r *http.Request) bool, pingPong centrifuge.PingPongConfig,
) *Handler {
upgrade := &websocket.Upgrader{
ReadBufferSize: c.ReadBufferSize,
EnableCompression: c.Compression,
}
if c.UseWriteBufferPool {
upgrade.WriteBufferPool = writeBufferPool
} else {
upgrade.WriteBufferSize = c.WriteBufferSize
}
if CheckOrigin != nil {
upgrade.CheckOrigin = CheckOrigin
} else {
upgrade.CheckOrigin = sameHostOriginCheck()
}
return &Handler{
node: n,
config: c,
upgrade: upgrade,
pingPong: pingPong,
}
}
func (s *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
compression := s.config.Compression
compressionLevel := s.config.CompressionLevel
compressionMinSize := s.config.CompressionMinSize
var req *protocol.ConnectRequest
connectRequestString := r.URL.Query().Get(connectUrlParam)
if connectRequestString != "" {
_, err := json.Parse(convert.StringToBytes(connectRequestString), &req, json.ZeroCopy)
if err != nil {
log.Info().Err(err).Str("transport", transportName).Msg("error unmarshalling connect request")
http.Error(rw, "invalid connect request", http.StatusBadRequest)
return
}
}
conn, _, err := s.upgrade.Upgrade(rw, r, nil)
if err != nil {
log.Error().Err(err).Str("transport", transportName).Msg("websocket upgrade error")
return
}
if compression {
err := conn.SetCompressionLevel(compressionLevel)
if err != nil {
log.Error().Err(err).Msg("websocket error setting compression level")
}
}
framePingInterval := s.pingPong.PingInterval
if framePingInterval <= 0 {
// Always use frame ping/pong in unidirectional WebSocket transport.
framePingInterval = DefaultWebsocketPingInterval
}
framePongTimeout := s.pingPong.PongTimeout
if framePongTimeout <= 0 {
framePongTimeout = DefaultWebsocketPongTimeout
}
writeTimeout := s.config.WriteTimeout.ToDuration()
if writeTimeout == 0 {
writeTimeout = DefaultWebsocketWriteTimeout
}
messageSizeLimit := s.config.MessageSizeLimit
if messageSizeLimit == 0 {
messageSizeLimit = DefaultWebsocketMessageSizeLimit
}
if messageSizeLimit > 0 {
conn.SetReadLimit(int64(messageSizeLimit))
}
if compression {
decompressedMessageSizeLimit := s.config.DecompressedMessageSizeLimit
if decompressedMessageSizeLimit == 0 {
decompressedMessageSizeLimit = messageSizeLimit * DefaultWebsocketDecompressedMessageSizeLimitMultiplier
}
if decompressedMessageSizeLimit > 0 {
conn.SetDecompressedReadLimit(int64(decompressedMessageSizeLimit))
}
}
// Separate goroutine for better GC of caller's data.
go func() {
// Record server-sent (outgoing) close codes for observability. Runs last
// (registered first), after any teardown close frame has been written.
// Client-supplied codes are skipped to keep metric cardinality bounded.
defer func() {
if code, incoming := conn.CloseCode(); code != 0 && !incoming {
s.node.IncTransportOutgoingClose(transportName, code)
}
}()
opts := websocketTransportOptions{
framePingInterval: framePingInterval,
framePongTimeout: framePongTimeout,
writeTimeout: writeTimeout,
compressionMinSize: compressionMinSize,
pingPongConfig: s.pingPong,
joinMessages: s.config.JoinPushMessages,
protoMajor: r.ProtoMajor,
disableClosingHandshake: s.config.DisableClosingHandshake,
disableDisconnectPush: s.config.DisableDisconnectPush,
}
graceCh := make(chan struct{})
transport := newWebsocketTransport(conn, opts, graceCh)
select {
case <-s.node.NotifyShutdown():
_ = transport.Close(centrifuge.DisconnectShutdown)
return
default:
}
ctxCh := make(chan struct{})
defer close(ctxCh)
c, closeFn, err := centrifuge.NewClient(NewCancelContext(r.Context(), ctxCh), s.node, transport)
if err != nil {
log.Error().Err(err).Str("transport", transportName).Msg("error creating client")
return
}
defer func() { _ = closeFn() }()
if logging.Enabled(logging.DebugLevel) {
log.Debug().Str("transport", transportName).Str("client", c.ID()).Msg("client connection established")
defer func(started time.Time) {
log.Debug().Str("transport", transportName).Str("client", c.ID()).
Str("duration", time.Since(started).String()).Msg("client connection completed")
}(time.Now())
}
pongWait := framePingInterval + framePongTimeout
_ = conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func([]byte) error {
_ = conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
waitClose := func() {
// https://github.com/gorilla/websocket/issues/448
conn.SetPingHandler(nil)
conn.SetPongHandler(nil)
if s.config.DisableClosingHandshake {
close(graceCh)
return
}
_ = conn.SetReadDeadline(time.Now().Add(closeFrameWait))
for {
if _, _, err := conn.NextReader(); err != nil {
close(graceCh)
return
}
}
}
if req == nil {
_, data, err := conn.ReadMessage()
if err != nil {
waitClose()
return
}
_, err = json.Parse(data, &req, json.ZeroCopy)
if err != nil {
log.Info().Err(err).Str("transport", transportName).Msg("error unmarshalling connect request")
if !s.config.DisableClosingHandshake {
err = conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(
int(centrifuge.DisconnectBadRequest.Code),
centrifuge.DisconnectBadRequest.Reason,
),
time.Now().Add(writeTimeout))
if err != nil {
return
}
}
waitClose()
return
}
}
if req == nil {
// A literal `null` connect frame decodes to a nil request with no error;
// treat it as an empty connect request. Passing nil to the connect
// handler nil-derefs, and this runs in a bare goroutine (ServeHTTP has
// already returned), so the panic would crash the whole process.
req = &protocol.ConnectRequest{}
}
c.ProtocolConnect(req)
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
waitClose()
}()
}