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

View File

@@ -0,0 +1,34 @@
package uniws
import (
"context"
"time"
)
// customCancelContext wraps context and cancels as soon as channel closed.
type customCancelContext struct {
context.Context
ch <-chan struct{}
}
// Deadline not used.
func (c customCancelContext) Deadline() (time.Time, bool) { return time.Time{}, false }
// Done returns channel that will be closed as soon as connection closed.
func (c customCancelContext) Done() <-chan struct{} { return c.ch }
// Err returns context error.
func (c customCancelContext) Err() error {
select {
case <-c.ch:
return context.Canceled
default:
return nil
}
}
// NewCancelContext returns a wrapper context around original context that will
// be canceled on channel close.
func NewCancelContext(ctx context.Context, ch <-chan struct{}) context.Context {
return customCancelContext{Context: ctx, ch: ch}
}

53
internal/uniws/config.go Normal file
View File

@@ -0,0 +1,53 @@
package uniws
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
)
// Defaults.
const (
DefaultWebsocketPingInterval = 25 * time.Second
DefaultWebsocketPongTimeout = 10 * time.Second
DefaultWebsocketWriteTimeout = 1 * time.Second
DefaultWebsocketMessageSizeLimit = 65536 // 64KB
)
// DefaultWebsocketDecompressedMessageSizeLimitMultiplier is the default factor
// applied to MessageSizeLimit to derive the maximum allowed decompressed message
// size when compression is negotiated and DecompressedMessageSizeLimit is not set
// explicitly. MessageSizeLimit alone only bounds the compressed bytes received on
// the wire, so a small compressed frame could otherwise be inflated into a much
// larger amount of memory (a "decompression bomb"). The multiplier leaves
// generous headroom for legitimately compressible messages while still rejecting
// extreme expansion ratios.
const DefaultWebsocketDecompressedMessageSizeLimitMultiplier = 10
type Config = configtypes.UniWebSocket
func sameHostOriginCheck() func(r *http.Request) bool {
return func(r *http.Request) bool {
err := checkSameHost(r)
return err == nil
}
}
func checkSameHost(r *http.Request) error {
origin := r.Header.Get("Origin")
if origin == "" {
return nil
}
u, err := url.Parse(origin)
if err != nil {
return fmt.Errorf("failed to parse Origin header %q: %w", origin, err)
}
if strings.EqualFold(r.Host, u.Host) {
return nil
}
return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host)
}

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()
}()
}

View File

@@ -0,0 +1,323 @@
package uniws
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifugo/v6/internal/middleware"
"github.com/centrifugal/centrifugo/v6/internal/websocket"
"github.com/centrifugal/centrifuge"
"github.com/centrifugal/protocol"
"github.com/stretchr/testify/require"
)
func ensureMessageHasClient(t *testing.T, data []byte) {
t.Log(string(data))
var reply protocol.Reply
err := json.Unmarshal(data, &reply)
require.NoError(t, err)
require.NotEmpty(t, reply.Connect.Client)
}
func TestUnidirectionalWebSocket(t *testing.T) {
t.Parallel()
node, err := centrifuge.New(centrifuge.Config{
ClientStaleCloseDelay: time.Second,
})
require.NoError(t, err)
t.Cleanup(func() { _ = node.Shutdown(context.Background()) })
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
return centrifuge.ConnectReply{
Credentials: &centrifuge.Credentials{},
}, nil
})
config := configtypes.UniWebSocket{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
WriteTimeout: configtypes.Duration(time.Second),
MessageSizeLimit: 65536,
}
pingPong := centrifuge.PingPongConfig{
PingInterval: 5 * time.Second,
PongTimeout: 1 * time.Second,
}
handler := NewHandler(node, config, func(r *http.Request) bool {
t.Logf("Checking origin: %s", r.Header.Get("Origin"))
return r.Header.Get("Origin") == "" || r.Header.Get("Origin") == "https://example.com"
}, pingPong)
server := httptest.NewServer(middleware.LogRequest(handler))
t.Cleanup(func() { server.Close() })
waitServer := func() {
t.Helper()
// Wait for server to start.
for {
t.Logf("Waiting for server to start: %s", server.URL)
resp, err := http.Get(server.URL)
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
_ = resp.Body.Close()
break
}
}
waitServer()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
t.Run("successful upgrade", func(t *testing.T) {
dialer := websocket.Dialer{}
conn, _, _, err := dialer.Dial(wsURL, http.Header{"Origin": []string{"https://example.com"}})
require.NoError(t, err)
defer func() { _ = conn.Close() }()
connectMsg := `{}`
err = conn.WriteMessage(websocket.TextMessage, []byte(connectMsg))
require.NoError(t, err)
_, data, err := conn.ReadMessage()
require.NoError(t, err)
ensureMessageHasClient(t, data)
})
t.Run("invalid connect request", func(t *testing.T) {
dialer := websocket.Dialer{}
conn, _, _, err := dialer.Dial(wsURL, http.Header{"Origin": []string{"https://example.com"}})
require.NoError(t, err)
defer func() { _ = conn.Close() }()
connectMsg := `invalid`
err = conn.WriteMessage(websocket.TextMessage, []byte(connectMsg))
require.NoError(t, err)
_, _, err = conn.ReadMessage()
require.Error(t, err)
})
t.Run("bad origin", func(t *testing.T) {
dialer := websocket.Dialer{}
_, _, _, err := dialer.Dial(wsURL, http.Header{"Origin": []string{"https://evil.com"}})
require.Error(t, err)
require.Contains(t, err.Error(), "bad handshake", err.Error())
})
t.Run("connect request in URL params", func(t *testing.T) {
connectReq := `{}`
params := url.Values{}
params.Set(connectUrlParam, connectReq)
dialer := websocket.Dialer{}
conn, _, _, err := dialer.Dial(wsURL+"?"+params.Encode(), nil)
require.NoError(t, err)
defer func() { _ = conn.Close() }()
_, data, err := conn.ReadMessage()
require.NoError(t, err)
ensureMessageHasClient(t, data)
})
t.Run("invalid connect request in URL params", func(t *testing.T) {
params := url.Values{}
params.Set(connectUrlParam, "invalid-json")
dialer := websocket.Dialer{}
_, _, _, err := dialer.Dial(wsURL+"?"+params.Encode(), nil)
require.Error(t, err)
require.Contains(t, err.Error(), "bad handshake", err.Error())
})
t.Run("connect request timeout", func(t *testing.T) {
t.Parallel()
dialer := websocket.Dialer{
HandshakeTimeout: 1 * time.Second,
}
conn, _, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer func() { _ = conn.Close() }()
started := time.Now()
err = conn.SetReadDeadline(time.Now().Add(30 * time.Second))
require.NoError(t, err)
_, _, err = conn.ReadMessage()
require.NoError(t, err) // Disconnect stale message.
_, _, err = conn.ReadMessage()
require.Error(t, err)
require.WithinDuration(t, started, time.Now(), 3*time.Second)
})
t.Run("ping pong cycle", func(t *testing.T) {
t.Parallel()
dialer := websocket.Dialer{}
conn, _, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer func() { _ = conn.Close() }()
err = conn.WriteMessage(websocket.TextMessage, []byte(`{}`))
require.NoError(t, err)
pingReceived := false
conn.SetPingHandler(func(appData []byte) error {
pingReceived = true
t.Log("websocket frame ping received")
return conn.WriteMessage(websocket.PongMessage, appData)
})
err = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
require.NoError(t, err)
for {
_, data, err := conn.ReadMessage()
if err != nil {
break
}
if string(data) == `{}` {
t.Logf("centrifugal protocol ping received: %s", string(data))
}
}
require.True(t, pingReceived, "Expected to receive ping message")
})
}
func TestUnidirectionalWebSocket_DecompressionBombRejected(t *testing.T) {
t.Parallel()
node, err := centrifuge.New(centrifuge.Config{})
require.NoError(t, err)
t.Cleanup(func() { _ = node.Shutdown(context.Background()) })
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
return centrifuge.ConnectReply{Credentials: &centrifuge.Credentials{}}, nil
})
// MessageSizeLimit 1024 -> decompressed limit = 1024 * 10 = 10240 bytes.
config := configtypes.UniWebSocket{
Compression: true,
MessageSizeLimit: 1024,
}
handler := NewHandler(node, config, func(r *http.Request) bool { return true }, centrifuge.PingPongConfig{})
server := httptest.NewServer(middleware.LogRequest(handler))
t.Cleanup(func() { server.Close() })
for {
resp, err := http.Get(server.URL)
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
_ = resp.Body.Close()
break
}
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
dialer := websocket.Dialer{EnableCompression: true}
conn, _, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer func() { _ = conn.Close() }()
require.NoError(t, conn.SetCompressionLevel(9))
// 64KB of zeros compresses to well under the 1024-byte compressed limit but
// inflates far past the 10240-byte decompressed limit, as the first
// (pre-auth) frame.
bomb := make([]byte, 64*1024)
require.NoError(t, conn.WriteMessage(websocket.BinaryMessage, bomb))
require.NoError(t, conn.SetReadDeadline(time.Now().Add(5*time.Second)))
_, _, err = conn.ReadMessage()
require.Error(t, err)
require.Truef(t, websocket.IsCloseError(err, websocket.CloseMessageTooBig),
"expected close %d, got %v", websocket.CloseMessageTooBig, err)
}
func TestUnidirectionalWebSocket_CloseFrameSent(t *testing.T) {
t.Parallel()
// Create a custom node to trigger server-side disconnect.
testNode, err := centrifuge.New(centrifuge.Config{})
require.NoError(t, err)
t.Cleanup(func() { _ = testNode.Shutdown(context.Background()) })
testNode.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
return centrifuge.ConnectReply{
Credentials: &centrifuge.Credentials{},
}, nil
})
testNode.OnConnect(func(client *centrifuge.Client) {
client.Disconnect(centrifuge.DisconnectConnectionLimit)
})
testHandler := NewHandler(testNode, Config{}, func(r *http.Request) bool {
return true
}, centrifuge.PingPongConfig{})
testServer := httptest.NewServer(middleware.LogRequest(testHandler))
t.Cleanup(func() { testServer.Close() })
testWsURL := "ws" + strings.TrimPrefix(testServer.URL, "http")
// Wait for test server to start.
for {
resp, err := http.Get(testServer.URL)
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
_ = resp.Body.Close()
break
}
dialer := websocket.Dialer{}
conn, _, _, err := dialer.Dial(testWsURL, nil)
require.NoError(t, err)
defer func() { _ = conn.Close() }()
// Send connect request.
err = conn.WriteMessage(websocket.TextMessage, []byte(`{}`))
require.NoError(t, err)
// Read connect reply.
_, data, err := conn.ReadMessage()
require.NoError(t, err)
ensureMessageHasClient(t, data)
require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second)))
// Read until connection is closed - server should send close frame.
var closeErr *websocket.CloseError
for {
_, _, err := conn.ReadMessage()
if err != nil {
t.Logf("ReadMessage error: %v", err)
closeErr = &websocket.CloseError{}
if ok := errors.As(err, &closeErr); ok {
t.Logf("Close frame received: code=%d, text=%s", closeErr.Code, closeErr.Text)
break
}
// Some other error occurred.
break
}
}
// Verify close frame was received with correct code and reason.
require.NotNil(t, closeErr, "Expected to receive close frame")
require.Equal(t, centrifuge.DisconnectConnectionLimit.Code, uint32(closeErr.Code))
require.Equal(t, centrifuge.DisconnectConnectionLimit.Reason, closeErr.Text)
}

266
internal/uniws/transport.go Normal file
View File

@@ -0,0 +1,266 @@
package uniws
import (
"sync"
"time"
"github.com/centrifugal/centrifugo/v6/internal/timers"
"github.com/centrifugal/centrifugo/v6/internal/tools"
"github.com/centrifugal/centrifugo/v6/internal/websocket"
"github.com/centrifugal/centrifuge"
"github.com/centrifugal/protocol"
)
// websocketTransport is a wrapper struct over websocket connection to fit session
// interface so client will accept it.
type websocketTransport struct {
mu sync.RWMutex
writeMu sync.Mutex // sync general write with unidirectional ping write.
conn *websocket.Conn
closeCh chan struct{}
graceCh chan struct{}
opts websocketTransportOptions
pingTimer *time.Timer
closed bool
}
type websocketTransportOptions struct {
framePingInterval time.Duration
framePongTimeout time.Duration
writeTimeout time.Duration
compressionMinSize int
pingPongConfig centrifuge.PingPongConfig
protoMajor int
joinMessages bool
disableClosingHandshake bool // TODO: consider keeping/removing for Centrifugo v7.
disableDisconnectPush bool
}
func newWebsocketTransport(conn *websocket.Conn, opts websocketTransportOptions, graceCh chan struct{}) *websocketTransport {
transport := &websocketTransport{
conn: conn,
closeCh: make(chan struct{}),
graceCh: graceCh,
opts: opts,
}
if opts.framePingInterval > 0 {
transport.addPing()
}
return transport
}
func (t *websocketTransport) ping() {
select {
case <-t.closeCh:
return
default:
if t.opts.framePongTimeout > 0 {
// It's safe to call SetReadDeadline concurrently with reader in separate goroutine.
// According to Go docs:
// SetReadDeadline sets the deadline for future Read calls and any currently-blocked Read call.
_ = t.conn.SetReadDeadline(time.Now().Add(t.opts.framePongTimeout))
}
err := t.conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(t.opts.writeTimeout))
if err != nil {
_ = t.Close(centrifuge.DisconnectWriteError)
return
}
if t.opts.writeTimeout > 0 && t.opts.protoMajor > 1 {
// WriteControl set a write deadline on the underlying HTTP/2 stream but,
// unlike writeData, nothing clears it here. An expired HTTP/2 deadline
// cannot be extended and permanently RSTs the stream, so an idle
// connection would be dropped ~writeTimeout after each ping. Clear it,
// serialized against writeData via writeMu.
t.writeMu.Lock()
derr := t.conn.NetConn().SetWriteDeadline(time.Time{})
t.writeMu.Unlock()
if derr != nil {
_ = t.Close(centrifuge.DisconnectWriteError)
return
}
}
t.addPing()
}
}
func (t *websocketTransport) addPing() {
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return
}
t.pingTimer = time.AfterFunc(t.opts.framePingInterval, t.ping)
t.mu.Unlock()
}
const transportName = "uni_websocket"
// Name returns name of transport.
func (t *websocketTransport) Name() string {
return transportName
}
// Protocol returns transport protocol.
func (t *websocketTransport) Protocol() centrifuge.ProtocolType {
return centrifuge.ProtocolTypeJSON
}
// ProtocolVersion returns transport protocol version.
func (t *websocketTransport) ProtocolVersion() centrifuge.ProtocolVersion {
return centrifuge.ProtocolVersion2
}
// Unidirectional returns whether transport is unidirectional.
func (t *websocketTransport) Unidirectional() bool {
return true
}
// DisabledPushFlags ...
func (t *websocketTransport) DisabledPushFlags() uint64 {
if t.opts.disableDisconnectPush {
return centrifuge.PushFlagDisconnect
}
return 0
}
// PingPongConfig ...
func (t *websocketTransport) PingPongConfig() centrifuge.PingPongConfig {
return t.opts.pingPongConfig
}
// Emulation ...
func (t *websocketTransport) Emulation() bool {
return false
}
// AcceptProtocol ...
func (t *websocketTransport) AcceptProtocol() string {
return tools.GetAcceptProtocolLabel(t.opts.protoMajor)
}
func (t *websocketTransport) writeData(data []byte) error {
if t.opts.compressionMinSize > 0 {
t.conn.EnableWriteCompression(len(data) > t.opts.compressionMinSize)
}
var messageType = websocket.TextMessage
if t.Protocol() == centrifuge.ProtocolTypeProtobuf {
messageType = websocket.BinaryMessage
}
t.writeMu.Lock()
if t.opts.writeTimeout > 0 {
_ = t.conn.SetWriteDeadline(time.Now().Add(t.opts.writeTimeout))
}
err := t.conn.WriteMessage(messageType, data)
if err != nil {
t.writeMu.Unlock()
return err
}
if t.opts.writeTimeout > 0 {
_ = t.conn.SetWriteDeadline(time.Time{})
if t.opts.protoMajor > 1 {
// For HTTP/2 connections, we need to actually clear the deadline on the underlying
// connection. The websocket Conn.SetWriteDeadline only sets a field, but doesn't
// clear the deadline that was already set on the underlying net.Conn during write.
// This is critical for HTTP/2 ResponseController where expired deadlines cannot be
// extended and will cause the stream to fail permanently.
err = t.conn.NetConn().SetWriteDeadline(time.Time{})
if err != nil {
t.writeMu.Unlock()
return err
}
}
}
t.writeMu.Unlock()
return nil
}
func (t *websocketTransport) writeMany(messages ...[]byte) error {
protoType := protocol.TypeJSON
if t.Protocol() == centrifuge.ProtocolTypeProtobuf {
protoType = protocol.TypeProtobuf
}
encoder := protocol.GetDataEncoder(protoType)
defer protocol.PutDataEncoder(protoType, encoder)
for i := range messages {
_ = encoder.Encode(messages[i])
}
return t.writeData(encoder.Finish())
}
// Write data to transport.
func (t *websocketTransport) Write(message []byte) error {
return t.WriteMany(message)
}
// WriteMany data to transport.
func (t *websocketTransport) WriteMany(messages ...[]byte) error {
select {
case <-t.closeCh:
return nil
default:
var err error
if t.opts.joinMessages {
if len(messages) == 1 && t.Protocol() == centrifuge.ProtocolTypeJSON {
// Fast path for one JSON message.
return t.writeData(messages[0])
}
err = t.writeMany(messages...)
if err != nil {
return err
}
} else {
for i := 0; i < len(messages); i++ {
err = t.writeData(messages[i])
if err != nil {
return err
}
}
}
return nil
}
}
const closeFrameWait = 5 * time.Second
// Close closes transport.
func (t *websocketTransport) Close(disconnect centrifuge.Disconnect) error {
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return nil
}
t.closed = true
if t.pingTimer != nil {
t.pingTimer.Stop()
}
close(t.closeCh)
t.mu.Unlock()
if t.opts.disableClosingHandshake {
return t.conn.Close()
}
if disconnect.Code != centrifuge.DisconnectConnectionClosed.Code {
msg := websocket.FormatCloseMessage(int(disconnect.Code), disconnect.Reason)
err := t.conn.WriteControl(websocket.CloseMessage, msg, time.Now().Add(t.opts.writeTimeout))
if err != nil {
return t.conn.Close()
}
select {
case <-t.graceCh:
default:
// Wait for closing handshake completion.
tm := timers.AcquireTimer(closeFrameWait)
select {
case <-t.graceCh:
case <-tm.C:
}
timers.ReleaseTimer(tm)
}
return t.conn.Close()
}
return t.conn.Close()
}