1877 lines
64 KiB
Go
1877 lines
64 KiB
Go
package client
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/centrifugal/centrifugo/v6/internal/clientcontext"
|
||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||
"github.com/centrifugal/centrifugo/v6/internal/jwtverify"
|
||
"github.com/centrifugal/centrifugo/v6/internal/proxy"
|
||
"github.com/centrifugal/centrifugo/v6/internal/tools"
|
||
|
||
"github.com/centrifugal/centrifuge"
|
||
"github.com/centrifugal/protocol"
|
||
"github.com/cristalhq/jwt/v5"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func generateTestRSAKeys(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
|
||
reader := rand.Reader
|
||
bitSize := 2048
|
||
key, err := rsa.GenerateKey(reader, bitSize)
|
||
require.NoError(t, err)
|
||
return key, &key.PublicKey
|
||
}
|
||
|
||
func getTokenBuilder(rsaPrivateKey *rsa.PrivateKey, hmacSecret string) *jwt.Builder {
|
||
var signer jwt.Signer
|
||
if rsaPrivateKey != nil {
|
||
signer, _ = jwt.NewSignerRS(jwt.RS256, rsaPrivateKey)
|
||
} else {
|
||
// For HS we do everything in tests with key `secret`.
|
||
key := []byte(hmacSecret)
|
||
signer, _ = jwt.NewSignerHS(jwt.HS256, key)
|
||
}
|
||
return jwt.NewBuilder(signer)
|
||
}
|
||
|
||
func getConnToken(user string, exp int64, rsaPrivateKey *rsa.PrivateKey) string {
|
||
builder := getTokenBuilder(rsaPrivateKey, "secret")
|
||
claims := &jwtverify.ConnectTokenClaims{
|
||
Base64Info: "e30=",
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
Subject: user,
|
||
},
|
||
}
|
||
if exp > 0 {
|
||
claims.ExpiresAt = jwt.NewNumericDate(time.Unix(exp, 0))
|
||
}
|
||
token, err := builder.Build(claims)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return token.String()
|
||
}
|
||
|
||
func getSubscribeToken(user string, channel string, exp int64, rsaPrivateKey *rsa.PrivateKey, hmacSecret string) string {
|
||
builder := getTokenBuilder(rsaPrivateKey, hmacSecret)
|
||
claims := &jwtverify.SubscribeTokenClaims{
|
||
SubscribeOptions: jwtverify.SubscribeOptions{
|
||
Base64Info: "e30=",
|
||
},
|
||
Channel: channel,
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
Subject: user,
|
||
},
|
||
}
|
||
if exp > 0 {
|
||
claims.ExpiresAt = jwt.NewNumericDate(time.Unix(exp, 0))
|
||
}
|
||
token, err := builder.Build(claims)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return token.String()
|
||
}
|
||
|
||
func getConnTokenHS(user string, exp int64) string {
|
||
return getConnToken(user, exp, nil)
|
||
}
|
||
|
||
func getSubscribeTokenHS(user string, channel string, exp int64) string {
|
||
return getSubscribeTokenHSWithSecret(user, channel, exp, "secret")
|
||
}
|
||
|
||
func getSubscribeTokenHSWithSecret(user string, channel string, exp int64, hmacSecret string) string {
|
||
return getSubscribeToken(user, channel, exp, nil, hmacSecret)
|
||
}
|
||
|
||
func emptyJWTVerifier(t *testing.T, cfgContainer *config.Container) *jwtverify.VerifierJWT {
|
||
verifier, err := jwtverify.NewTokenVerifierJWT(jwtverify.VerifierConfig{}, cfgContainer)
|
||
require.NoError(t, err)
|
||
return verifier
|
||
}
|
||
|
||
func hmacJWTVerifier(t *testing.T, cfgContainer *config.Container) *jwtverify.VerifierJWT {
|
||
return hmacJWTVerifierWithSecret(t, cfgContainer, "secret")
|
||
}
|
||
|
||
func hmacJWTVerifierWithSecret(t *testing.T, cfgContainer *config.Container, secret string) *jwtverify.VerifierJWT {
|
||
verifier, err := jwtverify.NewTokenVerifierJWT(jwtverify.VerifierConfig{
|
||
HMACSecretKey: secret,
|
||
}, cfgContainer)
|
||
require.NoError(t, err)
|
||
return verifier
|
||
}
|
||
|
||
func TestClientHandlerSetup(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.Insecure = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, emptyJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
err = h.Setup()
|
||
require.NoError(t, err)
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
}
|
||
|
||
// TestOnClientConnectingEmulatedHeadersGating verifies that the client's
|
||
// emulated headers are only kept in the connection context when some proxy may
|
||
// actually forward them (ProxyMap.UsesEmulatedHeaders). When no proxy uses
|
||
// emulated_headers we must not retain the map for the connection lifetime.
|
||
func TestOnClientConnectingEmulatedHeadersGating(t *testing.T) {
|
||
for _, uses := range []bool{true, false} {
|
||
node := tools.NodeWithMemoryEngine()
|
||
|
||
cfgContainer, err := config.NewContainer(config.DefaultConfig())
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, emptyJWTVerifier(t, cfgContainer), nil, &ProxyMap{
|
||
UsesEmulatedHeaders: uses,
|
||
})
|
||
|
||
var connectCtx context.Context
|
||
connectProxyHandler := func(ctx context.Context, _ centrifuge.ConnectEvent) (centrifuge.ConnectReply, proxy.ConnectExtra, error) {
|
||
connectCtx = ctx
|
||
return centrifuge.ConnectReply{Credentials: ¢rifuge.Credentials{UserID: "1"}}, proxy.ConnectExtra{}, nil
|
||
}
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Transport: tools.NewTestTransport(),
|
||
Headers: map[string]string{"x-app-token": "v"},
|
||
}, connectProxyHandler, false)
|
||
require.NoError(t, err)
|
||
|
||
// Context passed to the connect proxy call.
|
||
_, inConnectCtx := clientcontext.GetEmulatedHeadersFromContext(connectCtx)
|
||
require.Equal(t, uses, inConnectCtx, "connect-call emulated headers presence should match uses=%v", uses)
|
||
|
||
// Long-lived connection context.
|
||
inReplyCtx := false
|
||
if reply.Context != nil {
|
||
_, inReplyCtx = clientcontext.GetEmulatedHeadersFromContext(reply.Context)
|
||
}
|
||
require.Equal(t, uses, inReplyCtx, "connection-context emulated headers presence should match uses=%v", uses)
|
||
|
||
_ = node.Shutdown(context.Background())
|
||
}
|
||
}
|
||
|
||
func TestClientConnectingNoCredentialsNoToken(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, emptyJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Transport: tools.NewTestTransport(),
|
||
}, nil, false)
|
||
require.NoError(t, err)
|
||
require.Nil(t, reply.Credentials)
|
||
}
|
||
|
||
func TestClientConnectingNoCredentialsNoTokenInsecure(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.Insecure = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, emptyJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{}, nil, false)
|
||
require.NoError(t, err)
|
||
|
||
require.NotNil(t, reply.Credentials)
|
||
require.Equal(t, "", reply.Credentials.UserID)
|
||
}
|
||
|
||
func TestClientConnectNoCredentialsNoTokenAnonymous(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.AllowAnonymousConnectWithoutToken = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, emptyJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{}, nil, false)
|
||
require.NoError(t, err)
|
||
|
||
require.NotNil(t, reply.Credentials)
|
||
require.Equal(t, "", reply.Credentials.UserID)
|
||
}
|
||
|
||
func TestClientConnectWithMalformedToken(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
_, err = h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Token: "bad bad token",
|
||
}, nil, false)
|
||
require.Error(t, err)
|
||
}
|
||
|
||
func TestClientConnectWithValidTokenHMAC(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Token: getConnTokenHS("42", 0),
|
||
}, nil, false)
|
||
require.NoError(t, err)
|
||
|
||
require.NotNil(t, reply.Credentials)
|
||
require.Equal(t, "42", reply.Credentials.UserID)
|
||
require.Equal(t, int64(0), reply.Credentials.ExpireAt)
|
||
}
|
||
|
||
func TestClientConnectWithProxy(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
connectProxyHandler := func(context.Context, centrifuge.ConnectEvent) (centrifuge.ConnectReply, proxy.ConnectExtra, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "34",
|
||
ExpireAt: 14,
|
||
Info: []byte(`{"key": "value"}`),
|
||
},
|
||
Subscriptions: map[string]centrifuge.SubscribeOptions{"channel1": {}},
|
||
}, proxy.ConnectExtra{}, nil
|
||
}
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{}, connectProxyHandler, true)
|
||
require.NoError(t, err)
|
||
|
||
require.NotNil(t, reply.Credentials)
|
||
require.Equal(t, "34", reply.Credentials.UserID)
|
||
require.Equal(t, int64(14), reply.Credentials.ExpireAt)
|
||
require.False(t, reply.ClientSideRefresh)
|
||
require.Contains(t, reply.Subscriptions, "channel1")
|
||
}
|
||
|
||
func TestClientConnectAutoCacheRecoverServerSubs(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
opts := &cfg.Channel.WithoutNamespace
|
||
opts.HistorySize = 1
|
||
opts.HistoryTTL = configtypes.Duration(time.Hour)
|
||
opts.ForceRecovery = true
|
||
opts.ForceRecoveryMode = "cache"
|
||
opts.AutoCacheRecover = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
connectProxyHandler := func(context.Context, centrifuge.ConnectEvent) (centrifuge.ConnectReply, proxy.ConnectExtra, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{UserID: "34"},
|
||
Subscriptions: map[string]centrifuge.SubscribeOptions{
|
||
"channel1": {EnableRecovery: true, RecoveryMode: centrifuge.RecoveryModeCache},
|
||
},
|
||
}, proxy.ConnectExtra{}, nil
|
||
}
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{}, connectProxyHandler, true)
|
||
require.NoError(t, err)
|
||
require.Contains(t, reply.Subscriptions, "channel1")
|
||
require.True(t, reply.Subscriptions["channel1"].AutoCacheRecover, "auto_cache_recover must set AutoCacheRecover for server-side subscription")
|
||
}
|
||
|
||
func TestClientConnectNoAutoCacheRecoverByDefault(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
opts := &cfg.Channel.WithoutNamespace
|
||
opts.HistorySize = 1
|
||
opts.HistoryTTL = configtypes.Duration(time.Hour)
|
||
opts.ForceRecovery = true
|
||
opts.ForceRecoveryMode = "cache"
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
connectProxyHandler := func(context.Context, centrifuge.ConnectEvent) (centrifuge.ConnectReply, proxy.ConnectExtra, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{UserID: "34"},
|
||
Subscriptions: map[string]centrifuge.SubscribeOptions{
|
||
"channel1": {EnableRecovery: true, RecoveryMode: centrifuge.RecoveryModeCache},
|
||
},
|
||
}, proxy.ConnectExtra{}, nil
|
||
}
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{}, connectProxyHandler, true)
|
||
require.NoError(t, err)
|
||
require.Contains(t, reply.Subscriptions, "channel1")
|
||
require.False(t, reply.Subscriptions["channel1"].AutoCacheRecover, "AutoCacheRecover must stay off without auto_cache_recover")
|
||
}
|
||
|
||
func TestClientConnectWithValidTokenRSA(t *testing.T) {
|
||
privateKey, pubKey := generateTestRSAKeys(t)
|
||
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier, err := jwtverify.NewTokenVerifierJWT(jwtverify.VerifierConfig{
|
||
RSAPublicKey: pubKey,
|
||
}, cfgContainer)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Token: getConnToken("42", 0, privateKey),
|
||
}, nil, false)
|
||
require.NoError(t, err)
|
||
|
||
require.NotNil(t, reply.Credentials)
|
||
require.Equal(t, "42", reply.Credentials.UserID)
|
||
require.Equal(t, int64(0), reply.Credentials.ExpireAt)
|
||
}
|
||
|
||
func TestClientConnectWithExpiringToken(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.AllowAnonymousConnectWithoutToken = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Token: getConnTokenHS("42", time.Now().Unix()+10),
|
||
}, nil, false)
|
||
require.NoError(t, err)
|
||
|
||
require.NotNil(t, reply.Credentials)
|
||
require.Equal(t, "42", reply.Credentials.UserID)
|
||
require.True(t, reply.Credentials.ExpireAt > 0)
|
||
}
|
||
|
||
func TestClientConnectWithExpiredToken(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.AllowAnonymousConnectWithoutToken = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
_, err = h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Token: getConnTokenHS("42", 1525541722),
|
||
}, nil, false)
|
||
require.Equal(t, centrifuge.ErrorTokenExpired, err)
|
||
}
|
||
|
||
func TestClientSideRefresh(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.AllowAnonymousConnectWithoutToken = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
reply, _, err := h.OnRefresh(¢rifuge.Client{}, centrifuge.RefreshEvent{
|
||
Token: getConnTokenHS("", 123),
|
||
}, nil)
|
||
require.NoError(t, err)
|
||
require.True(t, reply.Expired)
|
||
|
||
_, _, err = h.OnRefresh(¢rifuge.Client{}, centrifuge.RefreshEvent{
|
||
Token: "invalid",
|
||
}, nil)
|
||
require.Error(t, err)
|
||
|
||
reply, _, err = h.OnRefresh(¢rifuge.Client{}, centrifuge.RefreshEvent{
|
||
Token: getConnTokenHS("", 2525637058),
|
||
}, nil)
|
||
require.NoError(t, err)
|
||
require.False(t, reply.Expired)
|
||
require.Equal(t, int64(2525637058), reply.ExpireAt)
|
||
}
|
||
|
||
func TestClientSideRefreshDifferentUser(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.AllowAnonymousConnectWithoutToken = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
|
||
_, _, err = h.OnRefresh(¢rifuge.Client{}, centrifuge.RefreshEvent{
|
||
Token: getConnTokenHS("42", 2525637058),
|
||
}, nil)
|
||
require.ErrorIs(t, err, centrifuge.DisconnectInvalidToken)
|
||
}
|
||
|
||
func TestClientUserPersonalChannel(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.SubscribeToUserPersonalChannel.Enabled = true
|
||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
|
||
{
|
||
Name: "user",
|
||
ChannelOptions: configtypes.ChannelOptions{},
|
||
},
|
||
}
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
var tests = []struct {
|
||
Name string
|
||
Namespace string
|
||
}{
|
||
{"ok_no_namespace", ""},
|
||
{"ok_with_namespace", "user"},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.Name, func(t *testing.T) {
|
||
cfg := cfgContainer.Config()
|
||
cfg.Client.SubscribeToUserPersonalChannel.Enabled = true
|
||
cfg.Client.SubscribeToUserPersonalChannel.PersonalChannelNamespace = tt.Namespace
|
||
err := cfgContainer.Reload(cfg)
|
||
require.NoError(t, err)
|
||
reply, err := h.OnClientConnecting(context.Background(), centrifuge.ConnectEvent{
|
||
Token: getConnTokenHS("42", 0),
|
||
}, nil, false)
|
||
require.NoError(t, err)
|
||
require.Contains(t, reply.Subscriptions, cfgContainer.PersonalChannel("42"))
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestClientSubscribeChannel(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "12",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "non_existing_namespace:test1",
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorUnknownChannel, err)
|
||
}
|
||
|
||
func TestClientSubscribeChannelNoPermission(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "12",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "test1",
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientSubscribeChannelUserLimitedError(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.UserLimitedChannels = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "12",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "test#13",
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientSubscribeChannelUserLimitedOK(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.UserLimitedChannels = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "12",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "test#12",
|
||
}, nil, nil)
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestClientSubscribeWithToken(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "12",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: "",
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: "invalid",
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("12", "$test1", 123),
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorTokenExpired, err)
|
||
|
||
reply, _, err := h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("12", "$test1", 0),
|
||
}, nil, nil)
|
||
require.NoError(t, err)
|
||
require.Zero(t, reply.Options.ExpireAt)
|
||
}
|
||
|
||
func TestClientSubscribeWithTokenAnonymous(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("", "$test1", 0),
|
||
}, nil, nil)
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestClientSideSubRefresh(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.AllowAnonymousConnectWithoutToken = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "12",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
reply, _, err := h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("12", "$test1", time.Now().Unix()+10),
|
||
}, nil, nil)
|
||
require.NoError(t, err)
|
||
require.True(t, reply.Options.ExpireAt > 0)
|
||
|
||
subRefreshReply, _, err := h.OnSubRefresh(client, nil, centrifuge.SubRefreshEvent{
|
||
Channel: "$test2",
|
||
Token: getSubscribeTokenHS("12", "$test1", 123),
|
||
})
|
||
require.NoError(t, err)
|
||
require.True(t, subRefreshReply.Expired)
|
||
|
||
subRefreshReply, _, err = h.OnSubRefresh(client, nil, centrifuge.SubRefreshEvent{
|
||
Channel: "$test2",
|
||
Token: "invalid",
|
||
})
|
||
require.Equal(t, centrifuge.DisconnectInvalidToken, err)
|
||
|
||
subRefreshReply, _, err = h.OnSubRefresh(client, nil, centrifuge.SubRefreshEvent{
|
||
Channel: "$test2",
|
||
Token: getSubscribeTokenHS("12", "$test1", 2525637058),
|
||
})
|
||
require.Equal(t, centrifuge.DisconnectInvalidToken, err)
|
||
|
||
subRefreshReply, _, err = h.OnSubRefresh(client, nil, centrifuge.SubRefreshEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("12", "$test1", 2525637058),
|
||
})
|
||
require.NoError(t, err)
|
||
require.False(t, subRefreshReply.Expired)
|
||
require.Equal(t, int64(2525637058), subRefreshReply.ExpireAt)
|
||
}
|
||
|
||
func TestClientSideSubRefresh_SeparateSubTokenConfig(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngine()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.AllowAnonymousConnectWithoutToken = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), hmacJWTVerifierWithSecret(t, cfgContainer, "new_secret"), &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "12",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
ok := centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("12", "$test1", time.Now().Unix()+10),
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
|
||
reply, _, err := h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHSWithSecret("12", "$test1", time.Now().Unix()+10, "new_secret"),
|
||
}, nil, nil)
|
||
require.NoError(t, err)
|
||
require.True(t, reply.Options.ExpireAt > 0)
|
||
|
||
subRefreshReply, _, err := h.OnSubRefresh(client, nil, centrifuge.SubRefreshEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHSWithSecret("12", "$test1", 2525637058, "new_secret"),
|
||
})
|
||
require.NoError(t, err)
|
||
require.False(t, subRefreshReply.Expired)
|
||
require.Equal(t, int64(2525637058), subRefreshReply.ExpireAt)
|
||
|
||
_, _, err = h.OnSubRefresh(client, nil, centrifuge.SubRefreshEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("12", "$test1", 2525637058),
|
||
})
|
||
require.Equal(t, centrifuge.DisconnectInvalidToken, err)
|
||
}
|
||
|
||
func TestClientSubscribePrivateChannelWithExpiringToken(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, _, err = h.OnSubscribe(¢rifuge.Client{}, centrifuge.SubscribeEvent{
|
||
Channel: "$test1",
|
||
Token: getSubscribeTokenHS("", "$test1", 10),
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorTokenExpired, err)
|
||
}
|
||
|
||
func TestClientSubscribePermissionDeniedForAnonymous(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.SubscribeForClient = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, _, err = h.OnSubscribe(¢rifuge.Client{}, centrifuge.SubscribeEvent{
|
||
Channel: "test1",
|
||
}, nil, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientPublishNotAllowed(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, err = h.OnPublish(¢rifuge.Client{}, centrifuge.PublishEvent{
|
||
Channel: "non_existing_namespace:test1",
|
||
Data: []byte(`{}`),
|
||
}, nil)
|
||
require.Equal(t, centrifuge.ErrorUnknownChannel, err)
|
||
|
||
_, err = h.OnPublish(¢rifuge.Client{}, centrifuge.PublishEvent{
|
||
Channel: "test1",
|
||
Data: []byte(`{}`),
|
||
}, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientPublishAllowed(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.PublishForClient = true
|
||
cfg.Channel.WithoutNamespace.PublishForAnonymous = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, err = h.OnPublish(¢rifuge.Client{}, centrifuge.PublishEvent{
|
||
Channel: "test1",
|
||
Data: []byte(`{}`),
|
||
}, nil)
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestClientPublishForSubscriber(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.PublishForSubscriber = true
|
||
cfg.Channel.WithoutNamespace.PublishForAnonymous = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, err = h.OnPublish(¢rifuge.Client{}, centrifuge.PublishEvent{
|
||
Channel: "test1",
|
||
Data: []byte(`{}`),
|
||
}, nil)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientHistory(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.HistorySize = 10
|
||
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(300 * time.Second)
|
||
cfg.Channel.WithoutNamespace.HistoryForClient = true
|
||
cfg.Channel.WithoutNamespace.HistoryForAnonymous = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
client.HandleCommand(&protocol.Command{Id: 1, Connect: &protocol.ConnectRequest{}}, 0)
|
||
require.NoError(t, client.Subscribe("test1"))
|
||
|
||
_, err = h.OnHistory(client, centrifuge.HistoryEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestClientHistoryError(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, err = h.OnHistory(¢rifuge.Client{}, centrifuge.HistoryEvent{
|
||
Channel: "non_existing_namespace:test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorUnknownChannel, err)
|
||
|
||
_, err = h.OnHistory(¢rifuge.Client{}, centrifuge.HistoryEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorNotAvailable, err)
|
||
|
||
cfg = cfgContainer.Config()
|
||
cfg.Channel.WithoutNamespace.HistorySize = 10
|
||
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(300 * time.Second)
|
||
require.NoError(t, cfgContainer.Reload(cfg))
|
||
|
||
_, err = h.OnHistory(¢rifuge.Client{}, centrifuge.HistoryEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientPresence(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.Presence = true
|
||
cfg.Channel.WithoutNamespace.PresenceForClient = true
|
||
cfg.Channel.WithoutNamespace.PresenceForAnonymous = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
client.HandleCommand(&protocol.Command{Id: 1, Connect: &protocol.ConnectRequest{}}, 0)
|
||
require.NoError(t, client.Subscribe("test1"))
|
||
|
||
_, err = h.OnPresence(client, centrifuge.PresenceEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestClientPresenceError(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, emptyJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, err = h.OnPresence(¢rifuge.Client{}, centrifuge.PresenceEvent{
|
||
Channel: "non_existing_namespace:test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorUnknownChannel, err)
|
||
|
||
_, err = h.OnPresence(¢rifuge.Client{}, centrifuge.PresenceEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorNotAvailable, err)
|
||
|
||
cfg = cfgContainer.Config()
|
||
cfg.Channel.WithoutNamespace.Presence = true
|
||
require.NoError(t, cfgContainer.Reload(cfg))
|
||
|
||
_, err = h.OnPresence(¢rifuge.Client{}, centrifuge.PresenceEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientPresenceStats(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{
|
||
Credentials: ¢rifuge.Credentials{
|
||
UserID: "",
|
||
},
|
||
}, nil
|
||
})
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.Presence = true
|
||
cfg.Channel.WithoutNamespace.PresenceForClient = true
|
||
cfg.Channel.WithoutNamespace.PresenceForAnonymous = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
client.HandleCommand(&protocol.Command{Id: 1, Connect: &protocol.ConnectRequest{}}, 0)
|
||
require.NoError(t, client.Subscribe("test1"))
|
||
|
||
_, err = h.OnPresenceStats(client, centrifuge.PresenceStatsEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func TestClientPresenceStatsError(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, emptyJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
_, err = h.OnPresenceStats(¢rifuge.Client{}, centrifuge.PresenceStatsEvent{
|
||
Channel: "non_existing_namespace:test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorUnknownChannel, err)
|
||
|
||
_, err = h.OnPresenceStats(¢rifuge.Client{}, centrifuge.PresenceStatsEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorNotAvailable, err)
|
||
|
||
cfg = cfgContainer.Config()
|
||
cfg.Channel.WithoutNamespace.Presence = true
|
||
require.NoError(t, cfgContainer.Reload(cfg))
|
||
|
||
_, err = h.OnPresenceStats(¢rifuge.Client{}, centrifuge.PresenceStatsEvent{
|
||
Channel: "test1",
|
||
})
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
}
|
||
|
||
func TestClientOnSubscribe_UserLimitedChannelDoesNotCallProxy(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.Proxy.Subscribe.Endpoint = "http://localhost:8080"
|
||
cfg.Channel.WithoutNamespace.UserLimitedChannels = true
|
||
cfg.Channel.WithoutNamespace.SubscribeProxyEnabled = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
h := NewHandler(node, cfgContainer, nil, nil, &ProxyMap{})
|
||
|
||
numProxyCalls := 0
|
||
|
||
proxyFunc := func(c proxy.Client, e centrifuge.SubscribeEvent, chOpts configtypes.ChannelOptions, pcd proxy.PerCallData) (centrifuge.SubscribeReply, proxy.SubscribeExtra, error) {
|
||
numProxyCalls++
|
||
return centrifuge.SubscribeReply{}, proxy.SubscribeExtra{}, nil
|
||
}
|
||
|
||
_, _, err = h.OnSubscribe(&tools.TestClientMock{
|
||
UserIDFunc: func() string {
|
||
return "42"
|
||
},
|
||
}, centrifuge.SubscribeEvent{
|
||
Channel: "user#42",
|
||
}, proxyFunc, nil)
|
||
require.NoError(t, err)
|
||
require.Equal(t, 0, numProxyCalls)
|
||
|
||
_, _, err = h.OnSubscribe(&tools.TestClientMock{
|
||
UserIDFunc: func() string {
|
||
return "42"
|
||
},
|
||
}, centrifuge.SubscribeEvent{
|
||
Channel: "user",
|
||
}, proxyFunc, nil)
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, numProxyCalls)
|
||
}
|
||
|
||
func TestClientOnSubscribe_UserLimitedChannelNotAllowedForAnotherUser(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.UserLimitedChannels = true
|
||
cfg.Channel.WithoutNamespace.SubscribeForClient = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
h := NewHandler(node, cfgContainer, nil, nil, &ProxyMap{})
|
||
|
||
numProxyCalls := 0
|
||
|
||
proxyFunc := func(c proxy.Client, e centrifuge.SubscribeEvent, chOpts configtypes.ChannelOptions, pcd proxy.PerCallData) (centrifuge.SubscribeReply, proxy.SubscribeExtra, error) {
|
||
numProxyCalls++
|
||
return centrifuge.SubscribeReply{}, proxy.SubscribeExtra{}, nil
|
||
}
|
||
|
||
_, _, err = h.OnSubscribe(&tools.TestClientMock{
|
||
IDFunc: func() string {
|
||
return "1"
|
||
},
|
||
UserIDFunc: func() string {
|
||
return "42"
|
||
},
|
||
}, centrifuge.SubscribeEvent{
|
||
Channel: "user#42",
|
||
}, proxyFunc, nil)
|
||
require.NoError(t, err)
|
||
|
||
_, _, err = h.OnSubscribe(&tools.TestClientMock{
|
||
IDFunc: func() string {
|
||
return "2"
|
||
},
|
||
UserIDFunc: func() string {
|
||
return "43"
|
||
},
|
||
}, centrifuge.SubscribeEvent{
|
||
Channel: "user#42",
|
||
}, proxyFunc, nil)
|
||
require.ErrorIs(t, err, centrifuge.ErrorPermissionDenied)
|
||
}
|
||
|
||
func TestClientOnSubscribe_SubRefreshProxy(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.Proxy.Subscribe.Endpoint = "http://localhost:8080"
|
||
cfg.Channel.WithoutNamespace.SubscribeProxyEnabled = true
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
h := NewHandler(node, cfgContainer, nil, nil, &ProxyMap{})
|
||
|
||
numProxyCalls := 0
|
||
|
||
proxyFunc := func(c proxy.Client, e centrifuge.SubscribeEvent, chOpts configtypes.ChannelOptions, pcd proxy.PerCallData) (centrifuge.SubscribeReply, proxy.SubscribeExtra, error) {
|
||
numProxyCalls++
|
||
return centrifuge.SubscribeReply{
|
||
ClientSideRefresh: !chOpts.SubRefreshProxyEnabled,
|
||
}, proxy.SubscribeExtra{}, nil
|
||
}
|
||
|
||
reply, _, err := h.OnSubscribe(&tools.TestClientMock{
|
||
UserIDFunc: func() string {
|
||
return "42"
|
||
},
|
||
}, centrifuge.SubscribeEvent{
|
||
Channel: "user",
|
||
}, proxyFunc, nil)
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, numProxyCalls)
|
||
require.True(t, reply.ClientSideRefresh)
|
||
|
||
cfg.Channel.WithoutNamespace.SubRefreshProxyEnabled = true
|
||
cfg.Channel.Proxy.SubRefresh.Endpoint = "https://example.com"
|
||
err = cfgContainer.Reload(cfg)
|
||
require.NoError(t, err)
|
||
reply, _, err = h.OnSubscribe(&tools.TestClientMock{
|
||
UserIDFunc: func() string {
|
||
return "42"
|
||
},
|
||
}, centrifuge.SubscribeEvent{
|
||
Channel: "user",
|
||
}, proxyFunc, nil)
|
||
require.NoError(t, err)
|
||
require.Equal(t, 2, numProxyCalls)
|
||
require.False(t, reply.ClientSideRefresh)
|
||
}
|
||
|
||
// TestClientOnSubscribe_StreamProxyStoresCancelFunc is a regression test for
|
||
// https://github.com/centrifugal/centrifugo/issues/1147 – the cancel function of a
|
||
// subscribe stream proxy must be stored in client storage for BOTH unidirectional and
|
||
// bidirectional streams. The OnUnsubscribe handler (registered in Setup) looks up the
|
||
// "stream_cancel_<channel>" key to cancel the stream context when a client unsubscribes.
|
||
// Previously the cancel func was only stored for bidirectional streams, so unidirectional
|
||
// streams stayed open until the whole connection was closed.
|
||
func TestClientOnSubscribe_StreamProxyStoresCancelFunc(t *testing.T) {
|
||
for _, bidirectional := range []bool{false, true} {
|
||
name := "unidirectional"
|
||
if bidirectional {
|
||
name = "bidirectional"
|
||
}
|
||
t.Run(name, func(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.WithoutNamespace.SubscribeStreamProxyEnabled = true
|
||
cfg.Channel.WithoutNamespace.SubscribeStreamBidirectional = bidirectional
|
||
cfg.Channel.Proxy.SubscribeStream.Endpoint = "grpc://localhost:12000"
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
h := NewHandler(node, cfgContainer, nil, nil, &ProxyMap{})
|
||
|
||
var cancelCalled bool
|
||
cancelFunc := func() { cancelCalled = true }
|
||
|
||
streamHandlerFunc := func(
|
||
c proxy.Client, bidi bool, e centrifuge.SubscribeEvent,
|
||
chOpts configtypes.ChannelOptions, pcd proxy.PerCallData,
|
||
) (centrifuge.SubscribeReply, proxy.StreamPublishFunc, func(), error) {
|
||
require.Equal(t, bidirectional, bidi)
|
||
return centrifuge.SubscribeReply{}, nil, cancelFunc, nil
|
||
}
|
||
|
||
client := &tools.TestClientMock{
|
||
IDFunc: func() string { return "42" },
|
||
UserIDFunc: func() string { return "42" },
|
||
ContextFunc: func() context.Context { return context.Background() },
|
||
}
|
||
|
||
_, _, err = h.OnSubscribe(client, centrifuge.SubscribeEvent{
|
||
Channel: "user",
|
||
}, nil, streamHandlerFunc)
|
||
require.NoError(t, err)
|
||
|
||
storage, release := client.AcquireStorage()
|
||
stored, ok := storage["stream_cancel_user"].(func())
|
||
release(storage)
|
||
require.True(t, ok, "cancel func must be stored so OnUnsubscribe can close the stream")
|
||
require.NotNil(t, stored)
|
||
|
||
// The stored func must be the one returned by the stream handler – invoking it
|
||
// (as OnUnsubscribe does) cancels the underlying stream context.
|
||
stored()
|
||
require.True(t, cancelCalled)
|
||
})
|
||
}
|
||
}
|
||
|
||
// buildSharedPollDispatch builds the dispatch closure identical to handler.go's Setup(),
|
||
// using plain SharedPollHandler functions instead of proxy.SharedPollRefreshHandler
|
||
// (avoids prometheus dependency in tests).
|
||
func buildSharedPollDispatch(
|
||
cfgContainer *config.Container,
|
||
handlers map[string]centrifuge.SharedPollHandler,
|
||
) func(ctx context.Context, event centrifuge.SharedPollEvent) (centrifuge.SharedPollResult, error) {
|
||
return func(ctx context.Context, event centrifuge.SharedPollEvent) (centrifuge.SharedPollResult, error) {
|
||
_, _, chOpts, found, err := cfgContainer.ChannelOptions(event.Channel)
|
||
if err != nil || !found {
|
||
return centrifuge.SharedPollResult{}, centrifuge.ErrorInternal
|
||
}
|
||
proxyName := chOpts.SharedPoll.ProxyName
|
||
if proxyName == "" {
|
||
proxyName = config.DefaultProxyName
|
||
}
|
||
handler, ok := handlers[proxyName]
|
||
if !ok {
|
||
return centrifuge.SharedPollResult{}, centrifuge.ErrorInternal
|
||
}
|
||
return handler(ctx, event)
|
||
}
|
||
}
|
||
|
||
func TestSharedPollRefreshProxyDispatch_Default(t *testing.T) {
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.Insecure = true
|
||
cfg.SharedPoll.HMACSecretKey = "test-secret"
|
||
cfg.Channel.WithoutNamespace.SubscriptionType = "shared_poll"
|
||
cfg.Channel.Proxy.SharedPollRefresh.Endpoint = "http://localhost:9999"
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
var defaultCalls int
|
||
handlers := map[string]centrifuge.SharedPollHandler{
|
||
"default": func(_ context.Context, event centrifuge.SharedPollEvent) (centrifuge.SharedPollResult, error) {
|
||
defaultCalls++
|
||
return centrifuge.SharedPollResult{}, nil
|
||
},
|
||
}
|
||
|
||
dispatch := buildSharedPollDispatch(cfgContainer, handlers)
|
||
|
||
// Channel without namespace should use default proxy.
|
||
_, err = dispatch(context.Background(), centrifuge.SharedPollEvent{
|
||
Channel: "test_channel",
|
||
Items: []centrifuge.SharedPollItem{{Key: "k1", Version: 0}},
|
||
})
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, defaultCalls)
|
||
}
|
||
|
||
func TestSharedPollRefreshProxyDispatch_NamedProxy(t *testing.T) {
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.Insecure = true
|
||
cfg.SharedPoll.HMACSecretKey = "test-secret"
|
||
cfg.Proxies = configtypes.NamedProxies{
|
||
{Name: "backend_a", Proxy: configtypes.Proxy{Endpoint: "http://backend-a:9999", Timeout: configtypes.Duration(time.Second)}},
|
||
{Name: "backend_b", Proxy: configtypes.Proxy{Endpoint: "http://backend-b:9999", Timeout: configtypes.Duration(time.Second)}},
|
||
}
|
||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
|
||
{
|
||
Name: "ns1",
|
||
ChannelOptions: configtypes.ChannelOptions{
|
||
SubscriptionType: "shared_poll",
|
||
SharedPoll: configtypes.SharedPollConfig{
|
||
ProxyName: "backend_a",
|
||
},
|
||
},
|
||
},
|
||
{
|
||
Name: "ns2",
|
||
ChannelOptions: configtypes.ChannelOptions{
|
||
SubscriptionType: "shared_poll",
|
||
SharedPoll: configtypes.SharedPollConfig{
|
||
ProxyName: "backend_b",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
var callsA, callsB int
|
||
handlers := map[string]centrifuge.SharedPollHandler{
|
||
"backend_a": func(_ context.Context, event centrifuge.SharedPollEvent) (centrifuge.SharedPollResult, error) {
|
||
callsA++
|
||
return centrifuge.SharedPollResult{}, nil
|
||
},
|
||
"backend_b": func(_ context.Context, event centrifuge.SharedPollEvent) (centrifuge.SharedPollResult, error) {
|
||
callsB++
|
||
return centrifuge.SharedPollResult{}, nil
|
||
},
|
||
}
|
||
|
||
dispatch := buildSharedPollDispatch(cfgContainer, handlers)
|
||
|
||
// Channel in ns1 should use backend_a.
|
||
_, err = dispatch(context.Background(), centrifuge.SharedPollEvent{
|
||
Channel: "ns1:ch1",
|
||
Items: []centrifuge.SharedPollItem{{Key: "k1", Version: 0}},
|
||
})
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, callsA)
|
||
require.Equal(t, 0, callsB)
|
||
|
||
// Channel in ns2 should use backend_b.
|
||
_, err = dispatch(context.Background(), centrifuge.SharedPollEvent{
|
||
Channel: "ns2:ch1",
|
||
Items: []centrifuge.SharedPollItem{{Key: "k1", Version: 0}},
|
||
})
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, callsA)
|
||
require.Equal(t, 1, callsB)
|
||
|
||
// Another call to ns1 should increment only backend_a.
|
||
_, err = dispatch(context.Background(), centrifuge.SharedPollEvent{
|
||
Channel: "ns1:ch2",
|
||
Items: []centrifuge.SharedPollItem{{Key: "k2", Version: 0}},
|
||
})
|
||
require.NoError(t, err)
|
||
require.Equal(t, 2, callsA)
|
||
require.Equal(t, 1, callsB)
|
||
}
|
||
|
||
func TestSharedPollRefreshProxyDispatch_FallbackToDefault(t *testing.T) {
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.Insecure = true
|
||
cfg.SharedPoll.HMACSecretKey = "test-secret"
|
||
cfg.Channel.Proxy.SharedPollRefresh.Endpoint = "http://localhost:9999"
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
|
||
var defaultCalls int
|
||
handlers := map[string]centrifuge.SharedPollHandler{
|
||
"default": func(_ context.Context, event centrifuge.SharedPollEvent) (centrifuge.SharedPollResult, error) {
|
||
defaultCalls++
|
||
return centrifuge.SharedPollResult{}, nil
|
||
},
|
||
}
|
||
|
||
dispatch := buildSharedPollDispatch(cfgContainer, handlers)
|
||
|
||
// Channel without shared_poll configured — proxy name will be empty → "default".
|
||
_, err = dispatch(context.Background(), centrifuge.SharedPollEvent{
|
||
Channel: "some_channel",
|
||
Items: []centrifuge.SharedPollItem{{Key: "k1", Version: 0}},
|
||
})
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, defaultCalls)
|
||
}
|
||
|
||
// TestOnTrackPreviousSecretKeyRotation checks that shared_poll
|
||
// hmac_previous_secret_key_valid_until bounds the previous key usage in real
|
||
// server time. The iat inside a signature is chosen by whoever minted it, so a
|
||
// holder of the rotated-out key must not be able to keep minting accepted
|
||
// signatures after the cutoff by backdating iat.
|
||
func TestOnTrackPreviousSecretKeyRotation(t *testing.T) {
|
||
const (
|
||
currentKey = "current-secret-key"
|
||
prevKey = "previous-secret-key"
|
||
channel = "shared:poll"
|
||
userID = "42"
|
||
)
|
||
keys := []string{"key1", "key2"}
|
||
|
||
newHandlerWithClient := func(t *testing.T, validUntil int64) (*Handler, *centrifuge.Client, func()) {
|
||
t.Helper()
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.SharedPoll.HMACSecretKey = currentKey
|
||
cfg.SharedPoll.HMACPreviousSecretKey = prevKey
|
||
cfg.SharedPoll.HMACPreviousSecretKeyValidUntil = validUntil
|
||
cfg.Channel.Proxy.SharedPollRefresh.Endpoint = "http://localhost:9999"
|
||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
|
||
{
|
||
Name: "shared",
|
||
ChannelOptions: configtypes.ChannelOptions{SubscriptionType: "shared_poll"},
|
||
},
|
||
}
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
node.OnConnecting(func(ctx context.Context, event centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{Credentials: ¢rifuge.Credentials{UserID: userID}}, nil
|
||
})
|
||
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(&protocol.Command{Id: 1, Connect: &protocol.ConnectRequest{}})
|
||
require.NoError(t, err)
|
||
require.True(t, centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20))
|
||
|
||
return h, client, func() {
|
||
_ = closeFn()
|
||
_ = node.Shutdown(context.Background())
|
||
}
|
||
}
|
||
|
||
track := func(h *Handler, client *centrifuge.Client, sig string) (centrifuge.TrackReply, error) {
|
||
return h.OnTrack(client, centrifuge.TrackEvent{
|
||
Channel: channel,
|
||
Batches: []centrifuge.TrackBatch{{
|
||
Items: []centrifuge.TrackItem{{Key: keys[0]}, {Key: keys[1]}},
|
||
Signature: sig,
|
||
}},
|
||
})
|
||
}
|
||
|
||
now := time.Now().Unix()
|
||
|
||
t.Run("previous key accepted inside grace period", func(t *testing.T) {
|
||
validUntil := now + 3600
|
||
h, client, cleanup := newHandlerWithClient(t, validUntil)
|
||
defer cleanup()
|
||
|
||
sig := makeTestSignature(prevKey, channel, keys, userID, now-60, now+300)
|
||
reply, err := track(h, client, sig)
|
||
require.NoError(t, err)
|
||
require.Len(t, reply.Batches, 1)
|
||
require.Equal(t, now+300, reply.Batches[0].ExpireAt)
|
||
})
|
||
|
||
t.Run("previous key rejected for iat after cutoff", func(t *testing.T) {
|
||
validUntil := now + 3600
|
||
h, client, cleanup := newHandlerWithClient(t, validUntil)
|
||
defer cleanup()
|
||
|
||
sig := makeTestSignature(prevKey, channel, keys, userID, validUntil+1, now+300)
|
||
_, err := track(h, client, sig)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
})
|
||
|
||
t.Run("previous key rejected after cutoff passed", func(t *testing.T) {
|
||
validUntil := now - 3600
|
||
h, client, cleanup := newHandlerWithClient(t, validUntil)
|
||
defer cleanup()
|
||
|
||
// Honest signature – iat tells the truth about when it was minted.
|
||
sig := makeTestSignature(prevKey, channel, keys, userID, now, now+300)
|
||
_, err := track(h, client, sig)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
|
||
// Backdated signature – minted now but claiming an iat before the cutoff.
|
||
// Must be rejected too, otherwise the cutoff protects nothing against
|
||
// someone who retained the rotated-out key.
|
||
sig = makeTestSignature(prevKey, channel, keys, userID, validUntil-10, now+300)
|
||
_, err = track(h, client, sig)
|
||
require.Equal(t, centrifuge.ErrorPermissionDenied, err)
|
||
})
|
||
|
||
t.Run("current key still accepted after cutoff passed", func(t *testing.T) {
|
||
validUntil := now - 3600
|
||
h, client, cleanup := newHandlerWithClient(t, validUntil)
|
||
defer cleanup()
|
||
|
||
sig := makeTestSignature(currentKey, channel, keys, userID, now, now+300)
|
||
reply, err := track(h, client, sig)
|
||
require.NoError(t, err)
|
||
require.Len(t, reply.Batches, 1)
|
||
require.Equal(t, now+300, reply.Batches[0].ExpireAt)
|
||
})
|
||
|
||
t.Run("previous key accepted when no cutoff configured", func(t *testing.T) {
|
||
h, client, cleanup := newHandlerWithClient(t, 0)
|
||
defer cleanup()
|
||
|
||
sig := makeTestSignature(prevKey, channel, keys, userID, now, now+300)
|
||
reply, err := track(h, client, sig)
|
||
require.NoError(t, err)
|
||
require.Len(t, reply.Batches, 1)
|
||
require.Equal(t, now+300, reply.Batches[0].ExpireAt)
|
||
})
|
||
}
|
||
|
||
func TestSingleConnection(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
node.OnConnect(func(client *centrifuge.Client) {
|
||
t.Logf("client connected: %s, user ID %s", client.ID(), client.UserID())
|
||
client.OnDisconnect(func(event centrifuge.DisconnectEvent) {
|
||
t.Logf("disconnected from %s, user ID %s", client.ID(), client.UserID())
|
||
})
|
||
})
|
||
|
||
makeConnectCommandData := func(userID string) []byte {
|
||
connectCommand := &protocol.Command{
|
||
Id: 1,
|
||
Connect: &protocol.ConnectRequest{
|
||
Token: getConnTokenHS(userID, 0),
|
||
},
|
||
}
|
||
encoder := protocol.NewJSONCommandEncoder()
|
||
data, err := encoder.Encode(connectCommand)
|
||
require.NoError(t, err)
|
||
return data
|
||
}
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Client.SubscribeToUserPersonalChannel.Enabled = true
|
||
cfg.Client.SubscribeToUserPersonalChannel.SingleConnection = true
|
||
cfg.Channel.WithoutNamespace.Presence = true
|
||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{}
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
verifier := hmacJWTVerifier(t, cfgContainer)
|
||
h := NewHandler(node, cfgContainer, verifier, nil, &ProxyMap{})
|
||
err = h.Setup()
|
||
require.NoError(t, err)
|
||
|
||
// First connection should work.
|
||
transport1 := tools.NewTestTransport()
|
||
client1, closeFn1, err := centrifuge.NewClient(context.Background(), node, transport1)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn1() }()
|
||
|
||
ok := centrifuge.HandleReadFrame(client1, bytes.NewReader(makeConnectCommandData("user42")), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
personalChannel := cfgContainer.PersonalChannel("user42")
|
||
t.Logf("personal_channel: %s", personalChannel)
|
||
require.Eventuallyf(t, func() bool {
|
||
res, err := node.PresenceStats(personalChannel)
|
||
if err != nil {
|
||
t.Logf("error getting presence stats: %v", err)
|
||
return false
|
||
}
|
||
return res.NumClients == 1 && res.NumUsers == 1
|
||
}, 5*time.Second, 100*time.Millisecond, "expected presence stats to show 1 client and 1 user")
|
||
|
||
// Single connection disabled should allow connections.
|
||
// Temporarily disable single connection
|
||
cfg = cfgContainer.Config()
|
||
cfg.Client.SubscribeToUserPersonalChannel.SingleConnection = false
|
||
err = cfgContainer.Reload(cfg)
|
||
require.NoError(t, err)
|
||
|
||
transport2 := tools.NewTestTransport()
|
||
client2, closeFn2, err := centrifuge.NewClient(context.Background(), node, transport2)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn2() }()
|
||
ok = centrifuge.HandleReadFrame(client2, bytes.NewReader(makeConnectCommandData("user42")), 1<<20)
|
||
require.True(t, ok)
|
||
require.Eventuallyf(t, func() bool {
|
||
res, err := node.PresenceStats(personalChannel)
|
||
if err != nil {
|
||
t.Logf("error getting presence stats: %v", err)
|
||
return false
|
||
}
|
||
return res.NumClients == 2 && res.NumUsers == 1
|
||
}, 5*time.Second, 100*time.Millisecond, "expected presence stats to show 1 client and 1 user")
|
||
|
||
cfg.Client.SubscribeToUserPersonalChannel.SingleConnection = true
|
||
err = cfgContainer.Reload(cfg)
|
||
require.NoError(t, err)
|
||
|
||
// Another user should be able to connect.
|
||
transport3 := tools.NewTestTransport()
|
||
client3, closeFn3, err := centrifuge.NewClient(context.Background(), node, transport3)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn3() }()
|
||
ok = centrifuge.HandleReadFrame(client3, bytes.NewReader(makeConnectCommandData("user43")), 1<<20)
|
||
require.True(t, ok)
|
||
require.Eventuallyf(t, func() bool {
|
||
res, err := node.PresenceStats(cfgContainer.PersonalChannel("user43")) // Note - another personal channel.
|
||
if err != nil {
|
||
t.Logf("error getting presence stats: %v", err)
|
||
return false
|
||
}
|
||
if res.NumClients != 1 || res.NumUsers != 1 {
|
||
return false
|
||
}
|
||
res, err = node.PresenceStats(personalChannel)
|
||
if err != nil {
|
||
t.Logf("error getting presence stats: %v", err)
|
||
return false
|
||
}
|
||
return res.NumClients == 2 && res.NumUsers == 1 // Same stats as in previous check.
|
||
}, 5*time.Second, 100*time.Millisecond, "expected presence stats to show 1 clients and 1 users in both personal channels")
|
||
|
||
// Now let's check single connection enforcement really works.
|
||
transport4 := tools.NewTestTransport()
|
||
client4, closeFn4, err := centrifuge.NewClient(context.Background(), node, transport4)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn4() }()
|
||
ok = centrifuge.HandleReadFrame(client4, bytes.NewReader(makeConnectCommandData("user42")), 1<<20)
|
||
require.True(t, ok)
|
||
|
||
require.Eventuallyf(t, func() bool {
|
||
res, err := node.PresenceStats(personalChannel)
|
||
if err != nil {
|
||
t.Logf("error getting presence stats: %v", err)
|
||
return false
|
||
}
|
||
return res.NumClients == 1 && res.NumUsers == 1
|
||
}, 5*time.Second, 100*time.Millisecond, "expected presence stats to show 1 client and 1 user")
|
||
}
|
||
|
||
// TestOnMapPublishDataFormat ensures client map publish applies the namespace
|
||
// publication_data_format policy, same as client stream publish does.
|
||
func TestOnMapPublishDataFormat(t *testing.T) {
|
||
cases := []struct {
|
||
format string
|
||
data []byte
|
||
wantReject bool
|
||
}{
|
||
{configtypes.PublicationDataFormatJSONObject, []byte(`{"v":1}`), false},
|
||
{configtypes.PublicationDataFormatJSONObject, []byte(`[1,2]`), true},
|
||
{configtypes.PublicationDataFormatJSON, []byte(`nope`), true},
|
||
{configtypes.PublicationDataFormatBinary, nil, false},
|
||
{"", nil, true}, // default format rejects empty data
|
||
}
|
||
for _, c := range cases {
|
||
t.Run("format="+c.format, func(t *testing.T) {
|
||
node := tools.NodeWithMemoryEngineNoHandlers()
|
||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||
|
||
cfg := config.DefaultConfig()
|
||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
|
||
{
|
||
Name: "mp",
|
||
ChannelOptions: configtypes.ChannelOptions{
|
||
SubscriptionType: "map",
|
||
PublicationDataFormat: c.format,
|
||
Map: configtypes.MapConfig{
|
||
Mode: "ephemeral",
|
||
KeyTTL: configtypes.Duration(time.Minute),
|
||
AllowPublishForClient: true,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
cfgContainer, err := config.NewContainer(cfg)
|
||
require.NoError(t, err)
|
||
h := NewHandler(node, cfgContainer, hmacJWTVerifier(t, cfgContainer), nil, &ProxyMap{})
|
||
|
||
node.OnConnecting(func(ctx context.Context, e centrifuge.ConnectEvent) (centrifuge.ConnectReply, error) {
|
||
return centrifuge.ConnectReply{Credentials: ¢rifuge.Credentials{UserID: "42"}}, nil
|
||
})
|
||
transport := tools.NewTestTransport()
|
||
client, closeFn, err := centrifuge.NewClient(context.Background(), node, transport)
|
||
require.NoError(t, err)
|
||
defer func() { _ = closeFn() }()
|
||
enc := protocol.NewJSONCommandEncoder()
|
||
data, err := enc.Encode(&protocol.Command{Id: 1, Connect: &protocol.ConnectRequest{}})
|
||
require.NoError(t, err)
|
||
require.True(t, centrifuge.HandleReadFrame(client, bytes.NewReader(data), 1<<20))
|
||
|
||
_, err = h.OnMapPublish(client, centrifuge.MapPublishEvent{
|
||
Channel: "mp:test", Key: "k", Data: c.data,
|
||
}, nil)
|
||
if c.wantReject {
|
||
require.Equal(t, centrifuge.ErrorBadRequest, err)
|
||
} else {
|
||
require.NoError(t, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestValidateSharedPollRefreshData ensures a shared poll refresh batch is
|
||
// rejected as a whole when the backend returns data not matching the channel
|
||
// publication_data_format, while removals and data-less "unchanged" items pass.
|
||
func TestValidateSharedPollRefreshData(t *testing.T) {
|
||
const ch = "sp:test"
|
||
|
||
t.Run("valid batch accepted", func(t *testing.T) {
|
||
err := validateSharedPollRefreshData(ch, configtypes.PublicationDataFormatJSONObject, []centrifuge.SharedPollRefreshItem{
|
||
{Key: "k1", Data: []byte(`{"v":1}`), Version: 1},
|
||
{Key: "k2", Data: []byte(`{"v":2}`), Version: 1},
|
||
})
|
||
require.NoError(t, err)
|
||
})
|
||
|
||
t.Run("one bad item rejects whole batch", func(t *testing.T) {
|
||
err := validateSharedPollRefreshData(ch, configtypes.PublicationDataFormatJSONObject, []centrifuge.SharedPollRefreshItem{
|
||
{Key: "k1", Data: []byte(`{"v":1}`), Version: 1},
|
||
{Key: "k2", Data: []byte(`[1,2]`), Version: 1},
|
||
{Key: "k3", Data: []byte(`{"v":3}`), Version: 1},
|
||
})
|
||
require.Equal(t, centrifuge.ErrorBadRequest, err)
|
||
})
|
||
|
||
t.Run("invalid json rejected", func(t *testing.T) {
|
||
err := validateSharedPollRefreshData(ch, configtypes.PublicationDataFormatJSON, []centrifuge.SharedPollRefreshItem{
|
||
{Key: "k1", Data: []byte(`not json`), Version: 1},
|
||
})
|
||
require.Equal(t, centrifuge.ErrorBadRequest, err)
|
||
})
|
||
|
||
t.Run("removals and data-less items skipped", func(t *testing.T) {
|
||
// Removals carry no data, and an unchanged key may come back without
|
||
// data — neither must trip the empty-data rule of the default format.
|
||
err := validateSharedPollRefreshData(ch, "", []centrifuge.SharedPollRefreshItem{
|
||
{Key: "k1", Removed: true},
|
||
{Key: "k2", Version: 7},
|
||
})
|
||
require.NoError(t, err)
|
||
})
|
||
|
||
t.Run("binary format allows anything", func(t *testing.T) {
|
||
err := validateSharedPollRefreshData(ch, configtypes.PublicationDataFormatBinary, []centrifuge.SharedPollRefreshItem{
|
||
{Key: "k1", Data: []byte{0x00, 0x01, 0xff}, Version: 1},
|
||
})
|
||
require.NoError(t, err)
|
||
})
|
||
}
|