snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
1340
internal/api/api.go
Normal file
1340
internal/api/api.go
Normal file
File diff suppressed because it is too large
Load Diff
747
internal/api/api_test.go
Normal file
747
internal/api/api_test.go
Normal file
@@ -0,0 +1,747 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/metrics"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Initialize metrics with a custom registry for tests to avoid conflicts
|
||||
registry := prometheus.NewRegistry()
|
||||
_ = metrics.Init(metrics.Config{
|
||||
Registerer: registry,
|
||||
})
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func nodeWithMemoryEngine() *centrifuge.Node {
|
||||
n, err := centrifuge.New(centrifuge.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = n.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type testSurveyCaller struct{}
|
||||
|
||||
func (t testSurveyCaller) Channels(_ context.Context, _ *ChannelsRequest) (map[string]*ChannelInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t testSurveyCaller) Connections(_ context.Context, _ *ConnectionsRequest) (map[string]*ConnectionInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestPublishAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Publish(context.Background(), &PublishRequest{})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
|
||||
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test"})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
|
||||
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test", Data: []byte("test")})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test:test", Data: []byte("test")})
|
||||
require.Equal(t, ErrorUnknownChannel, resp.Error)
|
||||
}
|
||||
|
||||
// TestBatchAPINilCommand guards against a crash on a nil batch command (e.g. a
|
||||
// JSON `null` array element). In parallel mode each command runs in its own
|
||||
// goroutine, so a nil-deref would escape net/http's recover and crash the node.
|
||||
func TestBatchAPINilCommand(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
for _, parallel := range []bool{false, true} {
|
||||
resp := api.Batch(context.Background(), &BatchRequest{
|
||||
Parallel: parallel,
|
||||
Commands: []*Command{
|
||||
nil,
|
||||
{Publish: &PublishRequest{Channel: "test", Data: []byte("x")}},
|
||||
},
|
||||
})
|
||||
require.Len(t, resp.Replies, 2)
|
||||
require.Equal(t, ErrorBadRequest, resp.Replies[0].Error)
|
||||
require.Nil(t, resp.Replies[1].Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Broadcast(context.Background(), &BroadcastRequest{})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
|
||||
// Empty data validation now happens per-channel
|
||||
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test"}})
|
||||
require.Nil(t, resp.Error)
|
||||
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[0].Error.Code)
|
||||
|
||||
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test"}, Data: []byte("test")})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test:test"}, Data: []byte("test")})
|
||||
require.Nil(t, resp.Error)
|
||||
require.Equal(t, ErrorUnknownChannel, resp.Result.Responses[0].Error)
|
||||
|
||||
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test", "test:test"}, Data: []byte("test")})
|
||||
require.Nil(t, resp.Error)
|
||||
require.Equal(t, ErrorUnknownChannel, resp.Result.Responses[1].Error)
|
||||
}
|
||||
|
||||
func TestPublishAPI_DataFormat_JSON(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatJSON
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
// Valid JSON should succeed
|
||||
resp := api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte(`{"key": "value"}`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
// Valid JSON array should succeed
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte(`[1, 2, 3]`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
// Valid JSON primitives should succeed
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte(`"string"`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte(`123`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte(`true`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte(`null`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
// Invalid JSON should fail
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte(`not valid json`),
|
||||
})
|
||||
require.NotNil(t, resp.Error)
|
||||
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
|
||||
|
||||
// Empty data with JSON format should fail
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte{},
|
||||
})
|
||||
require.NotNil(t, resp.Error)
|
||||
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
|
||||
}
|
||||
|
||||
func TestPublishAPI_DataFormat_Binary(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatBinary
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
// Binary data should succeed
|
||||
resp := api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte{0x01, 0x02, 0x03},
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
// Empty data with binary format should succeed
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte{},
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
// Any data should succeed with binary format
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte("any text data"),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestPublishAPI_DataFormat_Default(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
// Default format (empty string) - should reject empty data
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
// Non-empty data should succeed
|
||||
resp := api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte("any data"),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
|
||||
// Empty data should fail (current behavior preserved)
|
||||
resp = api.Publish(context.Background(), &PublishRequest{
|
||||
Channel: "test",
|
||||
Data: []byte{},
|
||||
})
|
||||
require.NotNil(t, resp.Error)
|
||||
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
|
||||
}
|
||||
|
||||
func TestBroadcastAPI_DataFormat_JSON(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatJSON
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
// Valid JSON should succeed
|
||||
resp := api.Broadcast(context.Background(), &BroadcastRequest{
|
||||
Channels: []string{"test", "test2"},
|
||||
Data: []byte(`{"key": "value"}`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
require.Nil(t, resp.Result.Responses[0].Error)
|
||||
require.Nil(t, resp.Result.Responses[1].Error)
|
||||
|
||||
// Invalid JSON should fail for all channels
|
||||
resp = api.Broadcast(context.Background(), &BroadcastRequest{
|
||||
Channels: []string{"test", "test2"},
|
||||
Data: []byte(`not valid json`),
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[0].Error.Code)
|
||||
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[1].Error.Code)
|
||||
}
|
||||
|
||||
func TestBroadcastAPI_DataFormat_Binary(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatBinary
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
// Empty data should succeed with binary format
|
||||
resp := api.Broadcast(context.Background(), &BroadcastRequest{
|
||||
Channels: []string{"test", "test2"},
|
||||
Data: []byte{},
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
require.Nil(t, resp.Result.Responses[0].Error)
|
||||
require.Nil(t, resp.Result.Responses[1].Error)
|
||||
}
|
||||
|
||||
func TestHistoryAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.History(context.Background(), &HistoryRequest{})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
resp = api.History(context.Background(), &HistoryRequest{Channel: "test"})
|
||||
require.Equal(t, ErrorNotAvailable, resp.Error)
|
||||
|
||||
cfg = cfgContainer.Config()
|
||||
cfg.Channel.WithoutNamespace.HistorySize = 1
|
||||
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(1 * time.Second)
|
||||
_ = cfgContainer.Reload(cfg)
|
||||
|
||||
resp = api.History(context.Background(), &HistoryRequest{Channel: "test"})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestHistoryRemoveAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.HistoryRemove(context.Background(), &HistoryRemoveRequest{})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
resp = api.HistoryRemove(context.Background(), &HistoryRemoveRequest{Channel: "test"})
|
||||
require.Equal(t, ErrorNotAvailable, resp.Error)
|
||||
|
||||
cfg = cfgContainer.Config()
|
||||
cfg.Channel.WithoutNamespace.HistorySize = 1
|
||||
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(time.Second)
|
||||
_ = cfgContainer.Reload(cfg)
|
||||
|
||||
resp = api.HistoryRemove(context.Background(), &HistoryRemoveRequest{Channel: "test"})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestPresenceAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Presence(context.Background(), &PresenceRequest{})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
resp = api.Presence(context.Background(), &PresenceRequest{Channel: "test"})
|
||||
|
||||
require.Equal(t, ErrorNotAvailable, resp.Error)
|
||||
|
||||
cfg = cfgContainer.Config()
|
||||
cfg.Channel.WithoutNamespace.Presence = true
|
||||
_ = cfgContainer.Reload(cfg)
|
||||
|
||||
resp = api.Presence(context.Background(), &PresenceRequest{Channel: "test"})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestPresenceStatsAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.PresenceStats(context.Background(), &PresenceStatsRequest{})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
resp = api.PresenceStats(context.Background(), &PresenceStatsRequest{Channel: "test"})
|
||||
require.Equal(t, ErrorNotAvailable, resp.Error)
|
||||
|
||||
cfg = cfgContainer.Config()
|
||||
cfg.Channel.WithoutNamespace.Presence = true
|
||||
_ = cfgContainer.Reload(cfg)
|
||||
|
||||
resp = api.PresenceStats(context.Background(), &PresenceStatsRequest{Channel: "test"})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestDisconnectAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Disconnect(context.Background(), &DisconnectRequest{
|
||||
User: "test",
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestUnsubscribeAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Unsubscribe(context.Background(), &UnsubscribeRequest{
|
||||
User: "test",
|
||||
Channel: "test",
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestRefreshAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Refresh(context.Background(), &RefreshRequest{
|
||||
User: "test",
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestSubscribeAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Subscribe(context.Background(), &SubscribeRequest{
|
||||
User: "test",
|
||||
Channel: "test",
|
||||
})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestInfoAPI(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Info(context.Background(), &InfoRequest{})
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
|
||||
// configWithNamespace returns a DefaultConfig with a single namespace having the
|
||||
// given name and subscription_type. Used to test the subscription_type gates on
|
||||
// publish/map_*/shared_poll_* API endpoints. Fills the minimum mandatory fields
|
||||
// (map.mode for map types, global shared_poll.hmac_secret_key for shared_poll)
|
||||
// so the config passes Validate().
|
||||
func configWithNamespace(name, subscriptionType string) config.Config {
|
||||
cfg := config.DefaultConfig()
|
||||
chOpts := configtypes.ChannelOptions{
|
||||
SubscriptionType: subscriptionType,
|
||||
}
|
||||
switch subscriptionType {
|
||||
case "map":
|
||||
// Persistent mode requires no key_ttl and is the simplest minimum.
|
||||
chOpts.Map = configtypes.MapConfig{Mode: "persistent"}
|
||||
case "map_clients", "map_users":
|
||||
// Persistent mode is forbidden here (presence entries need TTL-based
|
||||
// cleanup), so use ephemeral with a key_ttl.
|
||||
chOpts.Map = configtypes.MapConfig{
|
||||
Mode: "ephemeral",
|
||||
KeyTTL: configtypes.Duration(time.Minute),
|
||||
}
|
||||
case "shared_poll":
|
||||
// Global HMAC secret required for any shared_poll namespace.
|
||||
cfg.SharedPoll.HMACSecretKey = "test-secret"
|
||||
// Default refresh proxy must have an endpoint when no explicit
|
||||
// proxy_name is set on the namespace.
|
||||
cfg.Channel.Proxy.SharedPollRefresh = configtypes.Proxy{
|
||||
Endpoint: "http://127.0.0.1:0/test",
|
||||
Timeout: configtypes.Duration(time.Second),
|
||||
}
|
||||
chOpts.SharedPoll = configtypes.SharedPollConfig{Mode: "versioned"}
|
||||
}
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
|
||||
{
|
||||
Name: name,
|
||||
ChannelOptions: chOpts,
|
||||
},
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// TestPublishAPI_RejectsMapAndSharedPollNamespaces ensures the stream Publish
|
||||
// endpoint rejects calls aimed at namespaces configured for map or shared poll
|
||||
// subscriptions — those have their own dedicated APIs.
|
||||
func TestPublishAPI_RejectsMapAndSharedPollNamespaces(t *testing.T) {
|
||||
cases := []struct {
|
||||
subscriptionType string
|
||||
}{
|
||||
{"map"},
|
||||
{"map_clients"},
|
||||
{"map_users"},
|
||||
{"shared_poll"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.subscriptionType, func(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := configWithNamespace("ns", c.subscriptionType)
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.Publish(context.Background(), &PublishRequest{Channel: "ns:test", Data: []byte("data")})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapPublishAPI_RejectsNonMapNamespaces ensures map_publish rejects calls
|
||||
// against namespaces that aren't configured for map subscriptions.
|
||||
func TestMapPublishAPI_RejectsNonMapNamespaces(t *testing.T) {
|
||||
cases := []struct {
|
||||
subscriptionType string
|
||||
}{
|
||||
{""},
|
||||
{"stream"},
|
||||
{"shared_poll"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run("type="+c.subscriptionType, func(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := configWithNamespace("ns", c.subscriptionType)
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.MapPublish(context.Background(), &MapPublishRequest{
|
||||
Channel: "ns:test",
|
||||
Key: "k",
|
||||
Data: []byte(`{"v":1}`),
|
||||
})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapPublishAPI_RejectsInvalidKeyMode ensures key_mode is validated against
|
||||
// the allowed set ("", "if_new", "if_exists").
|
||||
func TestMapPublishAPI_RejectsInvalidKeyMode(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := configWithNamespace("ns", "map")
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
resp := api.MapPublish(context.Background(), &MapPublishRequest{
|
||||
Channel: "ns:test",
|
||||
Key: "k",
|
||||
Data: []byte(`{"v":1}`),
|
||||
KeyMode: "bogus",
|
||||
})
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
}
|
||||
|
||||
// TestMapAPIs_RejectNonMapNamespaces ensures the other map API endpoints
|
||||
// (remove / read_state / read_stream / stats / clear) reject calls against
|
||||
// namespaces that aren't configured for map subscriptions, instead of leaking
|
||||
// internal errors from the broker.
|
||||
func TestMapAPIs_RejectNonMapNamespaces(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := configWithNamespace("ns", "") // default = stream
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
rmResp := api.MapRemove(context.Background(), &MapRemoveRequest{Channel: "ns:test", Key: "k"})
|
||||
require.Equal(t, ErrorBadRequest, rmResp.Error)
|
||||
|
||||
rsResp := api.MapReadState(context.Background(), &MapReadStateRequest{Channel: "ns:test"})
|
||||
require.Equal(t, ErrorBadRequest, rsResp.Error)
|
||||
|
||||
rstrResp := api.MapReadStream(context.Background(), &MapReadStreamRequest{Channel: "ns:test"})
|
||||
require.Equal(t, ErrorBadRequest, rstrResp.Error)
|
||||
|
||||
stResp := api.MapStats(context.Background(), &MapStatsRequest{Channel: "ns:test"})
|
||||
require.Equal(t, ErrorBadRequest, stResp.Error)
|
||||
|
||||
clResp := api.MapClear(context.Background(), &MapClearRequest{Channel: "ns:test"})
|
||||
require.Equal(t, ErrorBadRequest, clResp.Error)
|
||||
}
|
||||
|
||||
// TestMapAPIs_RejectUnknownChannel ensures the map API endpoints return
|
||||
// ErrorUnknownChannel instead of leaking internal errors when the channel's
|
||||
// namespace doesn't exist.
|
||||
func TestMapAPIs_RejectUnknownChannel(t *testing.T) {
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
// "missing:test" — namespace "missing" not configured.
|
||||
pubResp := api.MapPublish(context.Background(), &MapPublishRequest{Channel: "missing:test", Key: "k", Data: []byte(`{}`)})
|
||||
require.Equal(t, ErrorUnknownChannel, pubResp.Error)
|
||||
|
||||
rmResp := api.MapRemove(context.Background(), &MapRemoveRequest{Channel: "missing:test", Key: "k"})
|
||||
require.Equal(t, ErrorUnknownChannel, rmResp.Error)
|
||||
|
||||
rsResp := api.MapReadState(context.Background(), &MapReadStateRequest{Channel: "missing:test"})
|
||||
require.Equal(t, ErrorUnknownChannel, rsResp.Error)
|
||||
|
||||
rstrResp := api.MapReadStream(context.Background(), &MapReadStreamRequest{Channel: "missing:test"})
|
||||
require.Equal(t, ErrorUnknownChannel, rstrResp.Error)
|
||||
|
||||
stResp := api.MapStats(context.Background(), &MapStatsRequest{Channel: "missing:test"})
|
||||
require.Equal(t, ErrorUnknownChannel, stResp.Error)
|
||||
|
||||
clResp := api.MapClear(context.Background(), &MapClearRequest{Channel: "missing:test"})
|
||||
require.Equal(t, ErrorUnknownChannel, clResp.Error)
|
||||
}
|
||||
|
||||
// nodeWithMapSupport builds a node able to actually serve map operations for
|
||||
// channels of the given config container, so that valid publishes really reach
|
||||
// the map broker instead of failing on options resolution.
|
||||
func nodeWithMapSupport(cfgContainer *config.Container) *centrifuge.Node {
|
||||
n, err := centrifuge.New(centrifuge.Config{
|
||||
Map: centrifuge.MapConfig{
|
||||
GetMapChannelOptions: func(channel string) centrifuge.MapChannelOptions {
|
||||
_, _, chOpts, ok, err := cfgContainer.ChannelOptions(channel)
|
||||
if err != nil || !ok {
|
||||
return centrifuge.MapChannelOptions{}
|
||||
}
|
||||
var mode centrifuge.MapMode
|
||||
switch chOpts.Map.Mode {
|
||||
case "ephemeral":
|
||||
mode = centrifuge.MapModeEphemeral
|
||||
case "recoverable":
|
||||
mode = centrifuge.MapModeRecoverable
|
||||
case "persistent":
|
||||
mode = centrifuge.MapModePersistent
|
||||
}
|
||||
return centrifuge.MapChannelOptions{Mode: mode, KeyTTL: chOpts.Map.KeyTTL.ToDuration()}
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = n.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// TestMapPublishAPI_DataFormat ensures map_publish applies the namespace
|
||||
// publication_data_format policy, same as the stream Publish endpoint does.
|
||||
func TestMapPublishAPI_DataFormat(t *testing.T) {
|
||||
cases := []struct {
|
||||
format string
|
||||
data []byte
|
||||
b64Data string
|
||||
wantReject bool
|
||||
}{
|
||||
{configtypes.PublicationDataFormatJSONObject, []byte(`{"v":1}`), "", false},
|
||||
{configtypes.PublicationDataFormatJSONObject, []byte(`[1,2]`), "", true},
|
||||
{configtypes.PublicationDataFormatJSONObject, []byte(`not-json-object`), "", true},
|
||||
{configtypes.PublicationDataFormatJSON, []byte(`[1,2]`), "", false},
|
||||
{configtypes.PublicationDataFormatJSON, []byte(`nope`), "", true},
|
||||
{configtypes.PublicationDataFormatBinary, nil, "", false},
|
||||
{"", []byte(`whatever`), "", false},
|
||||
{"", nil, "", true}, // default format rejects empty data
|
||||
// b64data must be validated after decoding, not before.
|
||||
{configtypes.PublicationDataFormatJSONObject, nil, base64.StdEncoding.EncodeToString([]byte(`{"v":1}`)), false},
|
||||
{configtypes.PublicationDataFormatJSONObject, nil, base64.StdEncoding.EncodeToString([]byte(`nope`)), true},
|
||||
}
|
||||
for i, c := range cases {
|
||||
t.Run(fmt.Sprintf("%d_format=%q", i, c.format), func(t *testing.T) {
|
||||
cfg := configWithNamespace("ns", "map")
|
||||
cfg.Channel.Namespaces[0].PublicationDataFormat = c.format
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
node := nodeWithMapSupport(cfgContainer)
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test"})
|
||||
resp := api.MapPublish(context.Background(), &MapPublishRequest{
|
||||
Channel: "ns:test", Key: "k", Data: c.data, B64Data: c.b64Data,
|
||||
})
|
||||
if c.wantReject {
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
} else {
|
||||
require.Nil(t, resp.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSharedPollPublishAPI_DataFormat ensures shared_poll_publish applies the
|
||||
// namespace publication_data_format policy.
|
||||
func TestSharedPollPublishAPI_DataFormat(t *testing.T) {
|
||||
cases := []struct {
|
||||
format string
|
||||
data []byte
|
||||
wantReject bool
|
||||
}{
|
||||
{configtypes.PublicationDataFormatJSONObject, []byte(`{"v":1}`), false},
|
||||
{configtypes.PublicationDataFormatJSONObject, []byte(`nope`), true},
|
||||
{configtypes.PublicationDataFormatJSON, []byte(`nope`), true},
|
||||
{configtypes.PublicationDataFormatBinary, nil, false},
|
||||
{"", nil, true}, // default format rejects empty data
|
||||
}
|
||||
for i, c := range cases {
|
||||
t.Run(fmt.Sprintf("%d_format=%q", i, c.format), func(t *testing.T) {
|
||||
cfg := configWithNamespace("ns", "shared_poll")
|
||||
cfg.Channel.Namespaces[0].PublicationDataFormat = c.format
|
||||
cfg.Channel.Namespaces[0].SharedPoll.PublishEnabled = true
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
node := nodeWithMemoryEngine()
|
||||
defer func() { _ = node.Shutdown(context.Background()) }()
|
||||
|
||||
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test"})
|
||||
resp := api.SharedPollPublish(context.Background(), &SharedPollPublishRequest{
|
||||
Channel: "ns:test", Key: "k", Version: 1, Data: c.data,
|
||||
})
|
||||
if c.wantReject {
|
||||
require.Equal(t, ErrorBadRequest, resp.Error)
|
||||
} else {
|
||||
// The test node has no shared poll manager, so a valid payload
|
||||
// gets past validation and fails later — the point is that it is
|
||||
// not rejected as a bad request.
|
||||
require.NotEqual(t, ErrorBadRequest, resp.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
314
internal/api/consuming.go
Normal file
314
internal/api/consuming.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// ConsumingHandlerConfig configures ConsumingHandler.
|
||||
type ConsumingHandlerConfig struct {
|
||||
UseOpenTelemetry bool
|
||||
}
|
||||
|
||||
// ConsumingHandler is responsible for processing API commands from asynchronous consumers.
|
||||
type ConsumingHandler struct {
|
||||
node *centrifuge.Node
|
||||
config ConsumingHandlerConfig
|
||||
api *Executor
|
||||
}
|
||||
|
||||
// NewConsumingHandler creates new ConsumingHandler.
|
||||
func NewConsumingHandler(n *centrifuge.Node, apiExecutor *Executor, c ConsumingHandlerConfig) *ConsumingHandler {
|
||||
h := &ConsumingHandler{
|
||||
node: n,
|
||||
config: c,
|
||||
api: apiExecutor,
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func logNonRetryableConsumingError(err error, method string) {
|
||||
log.Error().Err(err).Str("method", method).Msg("non retryable error during consuming, skip message")
|
||||
}
|
||||
|
||||
// ErrShutdown is returned to consumers for messages received after Node shutdown
|
||||
// started. Such messages must not be acknowledged: this node is no longer able to
|
||||
// deliver them, and depending on the broker a publish may still return success (a
|
||||
// memory broker never fails, so without this check a message would be committed
|
||||
// while going nowhere). Returning an error makes consumers leave the message
|
||||
// unacknowledged, so it's redelivered to another node or after restart.
|
||||
//
|
||||
// It wraps context.Canceled since consumers treat cancellation as a stop signal
|
||||
// and give up immediately instead of retrying with backoff until the shutdown
|
||||
// timeout is reached.
|
||||
var ErrShutdown = fmt.Errorf("node is shutting down: %w", context.Canceled)
|
||||
|
||||
type Dispatcher struct {
|
||||
handler *ConsumingHandler
|
||||
}
|
||||
|
||||
func NewDispatcher(handler *ConsumingHandler) *Dispatcher {
|
||||
return &Dispatcher{
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
// shuttingDown returns true once Node shutdown started.
|
||||
func (d *Dispatcher) shuttingDown() bool {
|
||||
select {
|
||||
case <-d.handler.node.NotifyShutdown():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Publish(ctx context.Context, req *apiproto.PublishRequest) error {
|
||||
if d.shuttingDown() {
|
||||
return ErrShutdown
|
||||
}
|
||||
resp := d.handler.api.Publish(ctx, req)
|
||||
if d.handler.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && resp.Error.Code == apiproto.ErrorInternal.Code {
|
||||
return resp.Error
|
||||
}
|
||||
if resp.Error != nil {
|
||||
logNonRetryableConsumingError(resp.Error, "publish")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Broadcast(ctx context.Context, req *apiproto.BroadcastRequest) error {
|
||||
if d.shuttingDown() {
|
||||
return ErrShutdown
|
||||
}
|
||||
resp := d.handler.api.Broadcast(ctx, req)
|
||||
if d.handler.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && resp.Error.Code == apiproto.ErrorInternal.Code {
|
||||
return resp.Error
|
||||
}
|
||||
if resp.Error != nil {
|
||||
logNonRetryableConsumingError(resp.Error, "broadcast")
|
||||
return nil
|
||||
}
|
||||
for _, response := range resp.Result.Responses {
|
||||
if response.Error != nil && response.Error.Code == apiproto.ErrorInternal.Code {
|
||||
// Any internal error in any channel response will result into a retry by a consumer.
|
||||
// To prevent duplicate messages publishers may use idempotency keys.
|
||||
return response.Error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ConsumedPublication struct {
|
||||
Data []byte
|
||||
// IdempotencyKey is used to prevent duplicate messages.
|
||||
IdempotencyKey string
|
||||
// Delta is used to indicate that the message is a delta update.
|
||||
Delta bool
|
||||
// Tags are used to attach metadata to the message.
|
||||
Tags map[string]string
|
||||
// Version of publication.
|
||||
Version uint64
|
||||
// VersionEpoch of publication.
|
||||
VersionEpoch string
|
||||
}
|
||||
|
||||
func (d *Dispatcher) DispatchPublication(
|
||||
ctx context.Context, channels []string, pub ConsumedPublication,
|
||||
) error {
|
||||
if d.shuttingDown() {
|
||||
return ErrShutdown
|
||||
}
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(channels) == 1 {
|
||||
req := &apiproto.PublishRequest{
|
||||
Data: pub.Data,
|
||||
Channel: channels[0],
|
||||
IdempotencyKey: pub.IdempotencyKey,
|
||||
Delta: pub.Delta,
|
||||
Tags: pub.Tags,
|
||||
Version: pub.Version,
|
||||
VersionEpoch: pub.VersionEpoch,
|
||||
}
|
||||
return d.Publish(ctx, req)
|
||||
}
|
||||
req := &apiproto.BroadcastRequest{
|
||||
Data: pub.Data,
|
||||
Channels: channels,
|
||||
IdempotencyKey: pub.IdempotencyKey,
|
||||
Delta: pub.Delta,
|
||||
Tags: pub.Tags,
|
||||
Version: pub.Version,
|
||||
VersionEpoch: pub.VersionEpoch,
|
||||
}
|
||||
return d.Broadcast(ctx, req)
|
||||
}
|
||||
|
||||
// Dispatch processes commands received from asynchronous consumers.
|
||||
func (d *Dispatcher) dispatchMethodPayload(ctx context.Context, method string, payload []byte) error {
|
||||
switch method {
|
||||
case "publish":
|
||||
_, err := d.handler.handlePublish(ctx, payload)
|
||||
if err != nil {
|
||||
var apiError *apiproto.Error
|
||||
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
|
||||
return err
|
||||
}
|
||||
logNonRetryableConsumingError(err, method)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case "broadcast":
|
||||
// This one is special as we need to iterate over responses. Ideally we want to use
|
||||
// code gen for Dispatch but need to make sure that broadcast logic is preserved.
|
||||
res, err := d.handler.handleBroadcast(ctx, payload)
|
||||
if err != nil {
|
||||
var apiError *apiproto.Error
|
||||
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
|
||||
return err
|
||||
}
|
||||
logNonRetryableConsumingError(err, method)
|
||||
return nil
|
||||
}
|
||||
for _, resp := range res.Responses {
|
||||
if resp.Error != nil && resp.Error.Code == apiproto.ErrorInternal.Code {
|
||||
// Any internal error in any channel response will result into a retry by a consumer.
|
||||
// To prevent duplicate messages publishers may use idempotency keys.
|
||||
return resp.Error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "subscribe":
|
||||
_, err := d.handler.handleSubscribe(ctx, payload)
|
||||
if err != nil {
|
||||
var apiError *apiproto.Error
|
||||
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
|
||||
return err
|
||||
}
|
||||
logNonRetryableConsumingError(err, method)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case "unsubscribe":
|
||||
_, err := d.handler.handleUnsubscribe(ctx, payload)
|
||||
if err != nil {
|
||||
var apiError *apiproto.Error
|
||||
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
|
||||
return err
|
||||
}
|
||||
logNonRetryableConsumingError(err, method)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case "disconnect":
|
||||
_, err := d.handler.handleDisconnect(ctx, payload)
|
||||
if err != nil {
|
||||
var apiError *apiproto.Error
|
||||
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
|
||||
return err
|
||||
}
|
||||
logNonRetryableConsumingError(err, method)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case "history_remove":
|
||||
_, err := d.handler.handleHistoryRemove(ctx, payload)
|
||||
if err != nil {
|
||||
var apiError *apiproto.Error
|
||||
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
|
||||
return err
|
||||
}
|
||||
logNonRetryableConsumingError(err, method)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case "refresh":
|
||||
_, err := d.handler.handleRefresh(ctx, payload)
|
||||
if err != nil {
|
||||
var apiError *apiproto.Error
|
||||
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
|
||||
return err
|
||||
}
|
||||
logNonRetryableConsumingError(err, method)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
// Skip unsupported.
|
||||
log.Info().Msg("skip unsupported API command method")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var ErrInvalidData = errors.New("invalid data")
|
||||
|
||||
// JSONRawOrString can decode payload from bytes and from JSON string. This gives
|
||||
// us better interoperability. For example, JSONB field is encoded as JSON string in
|
||||
// Debezium PostgreSQL connector.
|
||||
type JSONRawOrString json.RawMessage
|
||||
|
||||
func (j *JSONRawOrString) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 0 && data[0] == '"' {
|
||||
// Unmarshal as a string, then convert the string to json.RawMessage.
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return err
|
||||
}
|
||||
*j = JSONRawOrString(str)
|
||||
} else {
|
||||
// Unmarshal directly as json.RawMessage
|
||||
*j = data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns m as the JSON encoding of m.
|
||||
func (j JSONRawOrString) MarshalJSON() ([]byte, error) {
|
||||
if j == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return j, nil
|
||||
}
|
||||
|
||||
type MethodWithRequestPayload struct {
|
||||
Method string `json:"method"`
|
||||
Payload JSONRawOrString `json:"payload"`
|
||||
}
|
||||
|
||||
// DispatchCommand processes commands received from asynchronous consumers.
|
||||
func (d *Dispatcher) DispatchCommand(ctx context.Context, method string, payload []byte) error {
|
||||
if d.shuttingDown() {
|
||||
return ErrShutdown
|
||||
}
|
||||
if method != "" {
|
||||
// If method already known – we can skip decoding into MethodWithRequestPayload.
|
||||
return d.dispatchMethodPayload(ctx, method, payload)
|
||||
}
|
||||
// Otherwise we expect payload to be MethodWithRequestPayload.
|
||||
var e MethodWithRequestPayload
|
||||
err := json.Unmarshal(payload, &e)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("skip malformed consumed message")
|
||||
return nil
|
||||
}
|
||||
return d.dispatchMethodPayload(ctx, e.Method, e.Payload)
|
||||
}
|
||||
132
internal/api/consuming_gen.go
Normal file
132
internal/api/consuming_gen.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Code generated by internal/gen/api/main.go. DO NOT EDIT.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/metrics"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func (h *ConsumingHandler) handlePublish(ctx context.Context, data []byte) (*PublishResult, error) {
|
||||
req, err := requestDecoder.DecodePublish(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidData
|
||||
}
|
||||
resp := h.api.Publish(ctx, req)
|
||||
if h.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(h.api.config.Protocol, "publish", resp.Error.Code)
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (h *ConsumingHandler) handleBroadcast(ctx context.Context, data []byte) (*BroadcastResult, error) {
|
||||
req, err := requestDecoder.DecodeBroadcast(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidData
|
||||
}
|
||||
resp := h.api.Broadcast(ctx, req)
|
||||
if h.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(h.api.config.Protocol, "broadcast", resp.Error.Code)
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (h *ConsumingHandler) handleSubscribe(ctx context.Context, data []byte) (*SubscribeResult, error) {
|
||||
req, err := requestDecoder.DecodeSubscribe(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidData
|
||||
}
|
||||
resp := h.api.Subscribe(ctx, req)
|
||||
if h.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(h.api.config.Protocol, "subscribe", resp.Error.Code)
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (h *ConsumingHandler) handleUnsubscribe(ctx context.Context, data []byte) (*UnsubscribeResult, error) {
|
||||
req, err := requestDecoder.DecodeUnsubscribe(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidData
|
||||
}
|
||||
resp := h.api.Unsubscribe(ctx, req)
|
||||
if h.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(h.api.config.Protocol, "unsubscribe", resp.Error.Code)
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (h *ConsumingHandler) handleDisconnect(ctx context.Context, data []byte) (*DisconnectResult, error) {
|
||||
req, err := requestDecoder.DecodeDisconnect(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidData
|
||||
}
|
||||
resp := h.api.Disconnect(ctx, req)
|
||||
if h.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(h.api.config.Protocol, "disconnect", resp.Error.Code)
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (h *ConsumingHandler) handleHistoryRemove(ctx context.Context, data []byte) (*HistoryRemoveResult, error) {
|
||||
req, err := requestDecoder.DecodeHistoryRemove(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidData
|
||||
}
|
||||
resp := h.api.HistoryRemove(ctx, req)
|
||||
if h.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(h.api.config.Protocol, "history_remove", resp.Error.Code)
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (h *ConsumingHandler) handleRefresh(ctx context.Context, data []byte) (*RefreshResult, error) {
|
||||
req, err := requestDecoder.DecodeRefresh(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidData
|
||||
}
|
||||
resp := h.api.Refresh(ctx, req)
|
||||
if h.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(h.api.config.Protocol, "refresh", resp.Error.Code)
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
74
internal/api/consuming_test.go
Normal file
74
internal/api/consuming_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewConsumingHandler(t *testing.T) {
|
||||
n := nodeWithMemoryEngine()
|
||||
defer func() { _ = n.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := NewConsumingHandler(n, NewExecutor(n, cfgContainer, nil, ExecutorConfig{
|
||||
Protocol: "consumer",
|
||||
UseOpenTelemetry: false,
|
||||
}), ConsumingHandlerConfig{})
|
||||
|
||||
dispatcher := NewDispatcher(handler)
|
||||
|
||||
// Bad request must be just logged but no errors other than Internal Error should be returned from Dispatch.
|
||||
err = dispatcher.DispatchCommand(context.Background(), "publish", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDispatcherErrorsAfterNodeShutdown(t *testing.T) {
|
||||
n := nodeWithMemoryEngine()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
dispatcher := NewDispatcher(NewConsumingHandler(n, NewExecutor(n, cfgContainer, nil, ExecutorConfig{
|
||||
Protocol: "consumer",
|
||||
}), ConsumingHandlerConfig{}))
|
||||
|
||||
const payload = `{"channel": "test", "data": {}}`
|
||||
|
||||
// While node is running message must be dispatched successfully.
|
||||
require.NoError(t, dispatcher.DispatchCommand(context.Background(), "publish", []byte(payload)))
|
||||
|
||||
require.NoError(t, n.Shutdown(context.Background()))
|
||||
|
||||
// Memory broker keeps accepting publications even after Node shutdown – without
|
||||
// an explicit check a consumer would acknowledge a message which was never and
|
||||
// will never be delivered.
|
||||
err = dispatcher.DispatchCommand(context.Background(), "publish", []byte(payload))
|
||||
require.ErrorIs(t, err, ErrShutdown)
|
||||
// Consumers must stop processing instead of retrying until shutdown timeout.
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
|
||||
err = dispatcher.DispatchPublication(context.Background(), []string{"test"}, ConsumedPublication{
|
||||
Data: []byte(`{}`),
|
||||
})
|
||||
require.ErrorIs(t, err, ErrShutdown)
|
||||
|
||||
// Typed entry points must behave the same way.
|
||||
err = dispatcher.Publish(context.Background(), &apiproto.PublishRequest{
|
||||
Channel: "test", Data: []byte(`{}`),
|
||||
})
|
||||
require.ErrorIs(t, err, ErrShutdown)
|
||||
|
||||
err = dispatcher.Broadcast(context.Background(), &apiproto.BroadcastRequest{
|
||||
Channels: []string{"test"}, Data: []byte(`{}`),
|
||||
})
|
||||
require.ErrorIs(t, err, ErrShutdown)
|
||||
}
|
||||
77
internal/api/grpc.go
Normal file
77
internal/api/grpc.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
|
||||
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func authorize(ctx context.Context, key []byte) error {
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
if len(md["authorization"]) > 0 && subtle.ConstantTimeCompare([]byte(md["authorization"][0]), key) == 1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return status.Error(codes.Unauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
// GRPCKeyAuth allows to set simple authentication based on string key from configuration.
|
||||
// Client should provide per RPC credentials: set authorization key to metadata with value
|
||||
// `apikey <KEY>`.
|
||||
// Only installed when grpc_api.key is set – an unauthenticated GRPC API is the documented
|
||||
// default in v6, see the note in internal/app/grpc.go before reporting it as a vulnerability.
|
||||
func GRPCKeyAuth(key string) grpc.ServerOption {
|
||||
authKey := []byte("apikey " + key)
|
||||
return grpc.UnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
||||
if err := authorize(ctx, authKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handler(ctx, req)
|
||||
})
|
||||
}
|
||||
|
||||
// GRPCAPIServiceConfig for GRPC API Service.
|
||||
type GRPCAPIServiceConfig struct {
|
||||
UseOpenTelemetry bool
|
||||
UseTransportErrorMode bool
|
||||
}
|
||||
|
||||
// RegisterGRPCServerAPI registers GRPC API service in provided GRPC server.
|
||||
func RegisterGRPCServerAPI(n *centrifuge.Node, apiExecutor *Executor, server *grpc.Server, config GRPCAPIServiceConfig) error {
|
||||
RegisterCentrifugoApiServer(server, newGRPCAPIService(n, apiExecutor, config))
|
||||
return nil
|
||||
}
|
||||
|
||||
// grpcAPIService can answer on GRPC API requests.
|
||||
type grpcAPIService struct {
|
||||
UnimplementedCentrifugoApiServer
|
||||
|
||||
config GRPCAPIServiceConfig
|
||||
api *Executor
|
||||
}
|
||||
|
||||
// newGRPCAPIService creates new Service.
|
||||
func newGRPCAPIService(_ *centrifuge.Node, apiExecutor *Executor, c GRPCAPIServiceConfig) *grpcAPIService {
|
||||
return &grpcAPIService{
|
||||
config: c,
|
||||
api: apiExecutor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcAPIService) useTransportErrorMode(ctx context.Context) bool {
|
||||
if s.config.UseTransportErrorMode {
|
||||
return true
|
||||
}
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
errorMode := md.Get("x-centrifugo-error-mode")
|
||||
return len(errorMode) > 0 && errorMode[0] == "transport"
|
||||
}
|
||||
return false
|
||||
}
|
||||
400
internal/api/grpc_handler_gen.go
Normal file
400
internal/api/grpc_handler_gen.go
Normal file
@@ -0,0 +1,400 @@
|
||||
// Code generated by internal/gen/api/main.go. DO NOT EDIT.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/metrics"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Batch ...
|
||||
func (s *grpcAPIService) Batch(ctx context.Context, req *BatchRequest) (*BatchResponse, error) {
|
||||
resp := s.api.Batch(ctx, req)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Publish ...
|
||||
func (s *grpcAPIService) Publish(ctx context.Context, req *PublishRequest) (*PublishResponse, error) {
|
||||
resp := s.api.Publish(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Broadcast ...
|
||||
func (s *grpcAPIService) Broadcast(ctx context.Context, req *BroadcastRequest) (*BroadcastResponse, error) {
|
||||
resp := s.api.Broadcast(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Subscribe ...
|
||||
func (s *grpcAPIService) Subscribe(ctx context.Context, req *SubscribeRequest) (*SubscribeResponse, error) {
|
||||
resp := s.api.Subscribe(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Unsubscribe ...
|
||||
func (s *grpcAPIService) Unsubscribe(ctx context.Context, req *UnsubscribeRequest) (*UnsubscribeResponse, error) {
|
||||
resp := s.api.Unsubscribe(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Disconnect ...
|
||||
func (s *grpcAPIService) Disconnect(ctx context.Context, req *DisconnectRequest) (*DisconnectResponse, error) {
|
||||
resp := s.api.Disconnect(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Presence ...
|
||||
func (s *grpcAPIService) Presence(ctx context.Context, req *PresenceRequest) (*PresenceResponse, error) {
|
||||
resp := s.api.Presence(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// PresenceStats ...
|
||||
func (s *grpcAPIService) PresenceStats(ctx context.Context, req *PresenceStatsRequest) (*PresenceStatsResponse, error) {
|
||||
resp := s.api.PresenceStats(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// History ...
|
||||
func (s *grpcAPIService) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) {
|
||||
resp := s.api.History(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// HistoryRemove ...
|
||||
func (s *grpcAPIService) HistoryRemove(ctx context.Context, req *HistoryRemoveRequest) (*HistoryRemoveResponse, error) {
|
||||
resp := s.api.HistoryRemove(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Info ...
|
||||
func (s *grpcAPIService) Info(ctx context.Context, req *InfoRequest) (*InfoResponse, error) {
|
||||
resp := s.api.Info(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// RPC ...
|
||||
func (s *grpcAPIService) RPC(ctx context.Context, req *RPCRequest) (*RPCResponse, error) {
|
||||
resp := s.api.RPC(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Refresh ...
|
||||
func (s *grpcAPIService) Refresh(ctx context.Context, req *RefreshRequest) (*RefreshResponse, error) {
|
||||
resp := s.api.Refresh(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Channels ...
|
||||
func (s *grpcAPIService) Channels(ctx context.Context, req *ChannelsRequest) (*ChannelsResponse, error) {
|
||||
resp := s.api.Channels(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// MapPublish ...
|
||||
func (s *grpcAPIService) MapPublish(ctx context.Context, req *MapPublishRequest) (*MapPublishResponse, error) {
|
||||
resp := s.api.MapPublish(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// MapRemove ...
|
||||
func (s *grpcAPIService) MapRemove(ctx context.Context, req *MapRemoveRequest) (*MapRemoveResponse, error) {
|
||||
resp := s.api.MapRemove(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// MapReadState ...
|
||||
func (s *grpcAPIService) MapReadState(ctx context.Context, req *MapReadStateRequest) (*MapReadStateResponse, error) {
|
||||
resp := s.api.MapReadState(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// MapReadStream ...
|
||||
func (s *grpcAPIService) MapReadStream(ctx context.Context, req *MapReadStreamRequest) (*MapReadStreamResponse, error) {
|
||||
resp := s.api.MapReadStream(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// MapStats ...
|
||||
func (s *grpcAPIService) MapStats(ctx context.Context, req *MapStatsRequest) (*MapStatsResponse, error) {
|
||||
resp := s.api.MapStats(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// MapClear ...
|
||||
func (s *grpcAPIService) MapClear(ctx context.Context, req *MapClearRequest) (*MapClearResponse, error) {
|
||||
resp := s.api.MapClear(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SharedPollPublish ...
|
||||
func (s *grpcAPIService) SharedPollPublish(ctx context.Context, req *SharedPollPublishRequest) (*SharedPollPublishResponse, error) {
|
||||
resp := s.api.SharedPollPublish(ctx, req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
if resp.Error != nil && s.useTransportErrorMode(ctx) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
|
||||
statusCode := MapErrorToGRPCCode(resp.Error)
|
||||
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
|
||||
return nil, transportError.Err()
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
35
internal/api/grpc_test.go
Normal file
35
internal/api/grpc_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestGRPC_Authorize_Unauthenticated(t *testing.T) {
|
||||
err := authorize(context.Background(), []byte("apikey xxx"))
|
||||
require.Error(t, err)
|
||||
|
||||
md := metadata.New(map[string]string{})
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
err = authorize(ctx, []byte("apikey xxx"))
|
||||
require.Error(t, err)
|
||||
|
||||
md = metadata.New(map[string]string{
|
||||
"authorization": "apikey yyy",
|
||||
})
|
||||
ctx = metadata.NewIncomingContext(context.Background(), md)
|
||||
err = authorize(ctx, []byte("apikey xxx"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGRPC_Authorize_OK(t *testing.T) {
|
||||
md := metadata.New(map[string]string{
|
||||
"authorization": "apikey xxx",
|
||||
})
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
err := authorize(ctx, []byte("apikey xxx"))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
95
internal/api/handler.go
Normal file
95
internal/api/handler.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Config configures Handler.
|
||||
type Config struct {
|
||||
UseOpenTelemetry bool
|
||||
UseTransportErrorMode bool
|
||||
}
|
||||
|
||||
// Handler is responsible for processing API commands over HTTP.
|
||||
type Handler struct {
|
||||
mux *http.ServeMux
|
||||
node *centrifuge.Node
|
||||
config Config
|
||||
api *Executor
|
||||
}
|
||||
|
||||
var requestDecoder = NewJSONRequestDecoder()
|
||||
var responseEncoder = NewJSONResponseEncoder()
|
||||
|
||||
// NewHandler creates new Handler.
|
||||
func NewHandler(n *centrifuge.Node, apiExecutor *Executor, c Config) *Handler {
|
||||
m := new(http.ServeMux)
|
||||
h := &Handler{
|
||||
mux: m,
|
||||
node: n,
|
||||
config: c,
|
||||
api: apiExecutor,
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *Handler) Routes() map[string]http.HandlerFunc {
|
||||
return map[string]http.HandlerFunc{
|
||||
"/batch": s.handleBatch,
|
||||
"/publish": s.handlePublish,
|
||||
"/broadcast": s.handleBroadcast,
|
||||
"/subscribe": s.handleSubscribe,
|
||||
"/unsubscribe": s.handleUnsubscribe,
|
||||
"/disconnect": s.handleDisconnect,
|
||||
"/presence": s.handlePresence,
|
||||
"/presence_stats": s.handlePresenceStats,
|
||||
"/history": s.handleHistory,
|
||||
"/history_remove": s.handleHistoryRemove,
|
||||
"/info": s.handleInfo,
|
||||
"/rpc": s.handleRPC,
|
||||
"/refresh": s.handleRefresh,
|
||||
"/channels": s.handleChannels,
|
||||
"/map_publish": s.handleMapPublish,
|
||||
"/map_remove": s.handleMapRemove,
|
||||
"/map_read_state": s.handleMapReadState,
|
||||
"/map_read_stream": s.handleMapReadStream,
|
||||
"/map_stats": s.handleMapStats,
|
||||
"/map_clear": s.handleMapClear,
|
||||
"/shared_poll_publish": s.handleSharedPollPublish,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Handler) handleReadDataError(r *http.Request, w http.ResponseWriter, err error) {
|
||||
log.Error().Err(err).Str("path", r.URL.Path).Msg("error reading API request body")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (s *Handler) handleUnmarshalError(r *http.Request, w http.ResponseWriter, err error) {
|
||||
log.Error().Err(err).Str("path", r.URL.Path).Msg("error decoding API request body")
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *Handler) handleMarshalError(r *http.Request, w http.ResponseWriter, err error) {
|
||||
log.Error().Err(err).Str("path", r.URL.Path).Msg("error encoding API response")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (s *Handler) writeJson(w http.ResponseWriter, data []byte) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Handler) writeJsonCustomStatus(w http.ResponseWriter, statusCode int, data []byte) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Handler) useTransportErrorMode(r *http.Request) bool {
|
||||
return s.config.UseTransportErrorMode || r.Header.Get("X-Centrifugo-Error-Mode") == "transport"
|
||||
}
|
||||
881
internal/api/handler_gen.go
Normal file
881
internal/api/handler_gen.go
Normal file
@@ -0,0 +1,881 @@
|
||||
// Code generated by internal/gen/api/main.go. DO NOT EDIT.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/metrics"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func (s *Handler) handleBatch(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "batch", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeBatch(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "batch", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Batch(r.Context(), req)
|
||||
|
||||
data, err = responseEncoder.EncodeBatch(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "batch", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handlePublish(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "publish", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodePublish(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "publish", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Publish(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodePublish(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "publish", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleBroadcast(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "broadcast", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeBroadcast(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "broadcast", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Broadcast(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeBroadcast(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "broadcast", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleSubscribe(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "subscribe", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeSubscribe(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "subscribe", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Subscribe(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeSubscribe(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "subscribe", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleUnsubscribe(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "unsubscribe", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeUnsubscribe(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "unsubscribe", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Unsubscribe(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeUnsubscribe(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "unsubscribe", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleDisconnect(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "disconnect", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeDisconnect(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "disconnect", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Disconnect(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeDisconnect(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "disconnect", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handlePresence(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodePresence(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Presence(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodePresence(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handlePresenceStats(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence_stats", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodePresenceStats(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence_stats", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.PresenceStats(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodePresenceStats(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence_stats", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleHistory(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeHistory(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.History(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeHistory(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleHistoryRemove(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history_remove", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeHistoryRemove(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history_remove", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.HistoryRemove(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeHistoryRemove(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history_remove", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleInfo(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "info", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeInfo(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "info", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Info(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeInfo(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "info", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleRPC(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "rpc", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeRPC(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "rpc", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.RPC(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeRPC(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "rpc", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "refresh", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeRefresh(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "refresh", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Refresh(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeRefresh(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "refresh", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleChannels(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "channels", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeChannels(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "channels", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.Channels(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeChannels(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "channels", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleMapPublish(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_publish", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeMapPublish(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_publish", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.MapPublish(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeMapPublish(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_publish", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleMapRemove(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_remove", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeMapRemove(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_remove", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.MapRemove(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeMapRemove(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_remove", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleMapReadState(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_state", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeMapReadState(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_state", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.MapReadState(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeMapReadState(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_state", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleMapReadStream(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_stream", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeMapReadStream(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_stream", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.MapReadStream(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeMapReadStream(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_stream", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleMapStats(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_stats", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeMapStats(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_stats", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.MapStats(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeMapStats(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_stats", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleMapClear(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_clear", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeMapClear(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_clear", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.MapClear(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeMapClear(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_clear", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
|
||||
func (s *Handler) handleSharedPollPublish(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "shared_poll_publish", "read_body")
|
||||
s.handleReadDataError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := requestDecoder.DecodeSharedPollPublish(data)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "shared_poll_publish", "unmarshal")
|
||||
s.handleUnmarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.api.SharedPollPublish(r.Context(), req)
|
||||
if s.config.UseOpenTelemetry && resp.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, resp.Error.Error())
|
||||
}
|
||||
|
||||
if resp.Error != nil && s.useTransportErrorMode(r) {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
|
||||
statusCode := MapErrorToHTTPCode(resp.Error)
|
||||
data, _ = EncodeError(resp.Error)
|
||||
s.writeJsonCustomStatus(w, statusCode, data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = responseEncoder.EncodeSharedPollPublish(resp)
|
||||
if err != nil {
|
||||
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "shared_poll_publish", "marshal")
|
||||
s.handleMarshalError(r, w, err)
|
||||
return
|
||||
}
|
||||
if resp.Error != nil {
|
||||
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
|
||||
}
|
||||
|
||||
s.writeJson(w, data)
|
||||
}
|
||||
774
internal/api/handler_legacy.go
Normal file
774
internal/api/handler_legacy.go
Normal file
@@ -0,0 +1,774 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/apiproto"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type LegacyCommand struct {
|
||||
Id uint32 `json:"id,omitempty"`
|
||||
Method CommandMethodType `json:"method,omitempty"`
|
||||
Params apiproto.Raw `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type LegacyReply struct {
|
||||
Id uint32 `json:"id,omitempty"`
|
||||
Error *apiproto.Error `json:"error,omitempty"`
|
||||
Result apiproto.Raw `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
type CommandMethodType int32
|
||||
|
||||
// UnmarshalJSON helps to unmarshal command method when set as string.
|
||||
func (m *CommandMethodType) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 2 {
|
||||
if v, ok := Command_MethodType_value[strings.ToUpper(string(data[1:len(data)-1]))]; ok {
|
||||
*m = CommandMethodType(v)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
val, err := strconv.Atoi(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*m = CommandMethodType(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetParamsDecoder ...
|
||||
func GetParamsDecoder() apiproto.RequestDecoder {
|
||||
return apiproto.NewJSONRequestDecoder()
|
||||
}
|
||||
|
||||
// PutParamsDecoder ...
|
||||
func PutParamsDecoder(_ apiproto.RequestDecoder) {}
|
||||
|
||||
// GetResultEncoder ...
|
||||
func GetResultEncoder() apiproto.ResultEncoder {
|
||||
return apiproto.NewJSONResultEncoder()
|
||||
}
|
||||
|
||||
// PutResultEncoder ...
|
||||
func PutResultEncoder(_ apiproto.ResultEncoder) {}
|
||||
|
||||
var (
|
||||
jsonReplyEncoderPool sync.Pool
|
||||
)
|
||||
|
||||
// ReplyEncoder ...
|
||||
type ReplyEncoder interface {
|
||||
Reset()
|
||||
Encode(reply *LegacyReply) error
|
||||
Finish() []byte
|
||||
}
|
||||
|
||||
var _ ReplyEncoder = (*JSONReplyEncoder)(nil)
|
||||
|
||||
// JSONReplyEncoder ...
|
||||
type JSONReplyEncoder struct {
|
||||
count int
|
||||
buffer bytes.Buffer
|
||||
}
|
||||
|
||||
// NewJSONReplyEncoder ...
|
||||
func NewJSONReplyEncoder() *JSONReplyEncoder {
|
||||
return &JSONReplyEncoder{}
|
||||
}
|
||||
|
||||
// Reset ...
|
||||
func (e *JSONReplyEncoder) Reset() {
|
||||
e.count = 0
|
||||
e.buffer.Reset()
|
||||
}
|
||||
|
||||
// Encode ...
|
||||
func (e *JSONReplyEncoder) Encode(r *LegacyReply) error {
|
||||
if e.count > 0 {
|
||||
e.buffer.WriteString("\n")
|
||||
}
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.buffer.Write(data)
|
||||
e.count++
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finish ...
|
||||
func (e *JSONReplyEncoder) Finish() []byte {
|
||||
data := e.buffer.Bytes()
|
||||
dataCopy := make([]byte, len(data))
|
||||
copy(dataCopy, data)
|
||||
return dataCopy
|
||||
}
|
||||
|
||||
// GetReplyEncoder ...
|
||||
func GetReplyEncoder() ReplyEncoder {
|
||||
e := jsonReplyEncoderPool.Get()
|
||||
if e == nil {
|
||||
return NewJSONReplyEncoder()
|
||||
}
|
||||
return e.(ReplyEncoder)
|
||||
}
|
||||
|
||||
// PutReplyEncoder ...
|
||||
func PutReplyEncoder(e ReplyEncoder) {
|
||||
e.Reset()
|
||||
jsonReplyEncoderPool.Put(e)
|
||||
}
|
||||
|
||||
// CommandDecoder ...
|
||||
type CommandDecoder interface {
|
||||
Reset([]byte)
|
||||
Decode() (*LegacyCommand, error)
|
||||
}
|
||||
|
||||
var jsonCommandDecoderPool sync.Pool
|
||||
|
||||
var _ CommandDecoder = (*JSONCommandDecoder)(nil)
|
||||
|
||||
// GetCommandDecoder ...
|
||||
func GetCommandDecoder(data []byte) CommandDecoder {
|
||||
e := jsonCommandDecoderPool.Get()
|
||||
if e == nil {
|
||||
return NewJSONCommandDecoder(data)
|
||||
}
|
||||
decoder := e.(CommandDecoder)
|
||||
decoder.Reset(data)
|
||||
return decoder
|
||||
}
|
||||
|
||||
// PutCommandDecoder ...
|
||||
func PutCommandDecoder(d CommandDecoder) {
|
||||
jsonCommandDecoderPool.Put(d)
|
||||
}
|
||||
|
||||
// JSONCommandDecoder ...
|
||||
type JSONCommandDecoder struct {
|
||||
decoder *json.Decoder
|
||||
}
|
||||
|
||||
// NewJSONCommandDecoder ...
|
||||
func NewJSONCommandDecoder(data []byte) *JSONCommandDecoder {
|
||||
return &JSONCommandDecoder{
|
||||
decoder: json.NewDecoder(bytes.NewReader(data)),
|
||||
}
|
||||
}
|
||||
|
||||
// Reset ...
|
||||
func (d *JSONCommandDecoder) Reset(data []byte) {
|
||||
d.decoder = json.NewDecoder(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// Decode ...
|
||||
func (d *JSONCommandDecoder) Decode() (*LegacyCommand, error) {
|
||||
var c LegacyCommand
|
||||
err := d.decoder.Decode(&c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// OldRoute handles all methods inside one /api handler.
|
||||
// The plan is to remove it in Centrifugo v7.
|
||||
func (s *Handler) OldRoute() http.HandlerFunc {
|
||||
return s.handleAPI
|
||||
}
|
||||
|
||||
func (s *Handler) handleAPI(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-s.node.NotifyShutdown():
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
data, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error reading API request body")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
log.Error().Err(errors.New("no data in API request")).Msg("bad request")
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
encoder := GetReplyEncoder()
|
||||
defer PutReplyEncoder(encoder)
|
||||
|
||||
decoder := GetCommandDecoder(data)
|
||||
defer PutCommandDecoder(decoder)
|
||||
|
||||
for {
|
||||
command, decodeErr := decoder.Decode()
|
||||
if decodeErr != nil && decodeErr != io.EOF {
|
||||
log.Error().Err(decodeErr).Msg("error decoding API data")
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if command != nil {
|
||||
if s.config.UseOpenTelemetry {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetAttributes(attribute.String("method", Command_MethodType_name[int32(command.Method)]))
|
||||
}
|
||||
rep, err := s.handleAPICommand(r.Context(), command)
|
||||
if err != nil {
|
||||
if s.config.UseOpenTelemetry {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
}
|
||||
log.Error().Err(err).Msg("error handling API command")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if s.config.UseOpenTelemetry && rep.Error != nil {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetStatus(codes.Error, rep.Error.Error())
|
||||
}
|
||||
err = encoder.Encode(rep)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error encoding API reply")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
if decodeErr == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.writeJson(w, encoder.Finish())
|
||||
}
|
||||
|
||||
const (
|
||||
Command_PUBLISH CommandMethodType = 0
|
||||
Command_BROADCAST CommandMethodType = 1
|
||||
Command_UNSUBSCRIBE CommandMethodType = 2
|
||||
Command_DISCONNECT CommandMethodType = 3
|
||||
Command_PRESENCE CommandMethodType = 4
|
||||
Command_PRESENCE_STATS CommandMethodType = 5
|
||||
Command_HISTORY CommandMethodType = 6
|
||||
Command_HISTORY_REMOVE CommandMethodType = 7
|
||||
Command_CHANNELS CommandMethodType = 8
|
||||
Command_INFO CommandMethodType = 9
|
||||
Command_RPC CommandMethodType = 10
|
||||
Command_SUBSCRIBE CommandMethodType = 11
|
||||
Command_REFRESH CommandMethodType = 12
|
||||
Command_CONNECTIONS CommandMethodType = 14
|
||||
Command_UPDATE_USER_STATUS CommandMethodType = 15
|
||||
Command_GET_USER_STATUS CommandMethodType = 16
|
||||
Command_DELETE_USER_STATUS CommandMethodType = 17
|
||||
Command_BLOCK_USER CommandMethodType = 18
|
||||
Command_UNBLOCK_USER CommandMethodType = 19
|
||||
Command_REVOKE_TOKEN CommandMethodType = 20
|
||||
Command_INVALIDATE_USER_TOKENS CommandMethodType = 21
|
||||
Command_DEVICE_REGISTER CommandMethodType = 22
|
||||
Command_DEVICE_UPDATE CommandMethodType = 23
|
||||
Command_DEVICE_REMOVE CommandMethodType = 24
|
||||
Command_DEVICE_LIST CommandMethodType = 25
|
||||
Command_DEVICE_TOPIC_LIST CommandMethodType = 26
|
||||
Command_DEVICE_TOPIC_UPDATE CommandMethodType = 27
|
||||
Command_USER_TOPIC_LIST CommandMethodType = 28
|
||||
Command_USER_TOPIC_UPDATE CommandMethodType = 29
|
||||
Command_SEND_PUSH_NOTIFICATION CommandMethodType = 30
|
||||
Command_UPDATE_PUSH_STATUS CommandMethodType = 31
|
||||
Command_CANCEL_PUSH CommandMethodType = 32
|
||||
Command_MAP_PUBLISH CommandMethodType = 33
|
||||
Command_MAP_REMOVE CommandMethodType = 34
|
||||
Command_MAP_READ_STATE CommandMethodType = 35
|
||||
Command_MAP_READ_STREAM CommandMethodType = 36
|
||||
Command_MAP_STATS CommandMethodType = 37
|
||||
Command_MAP_CLEAR CommandMethodType = 38
|
||||
Command_SHARED_POLL_PUBLISH CommandMethodType = 39
|
||||
)
|
||||
|
||||
var Command_MethodType_value = map[string]int32{
|
||||
"PUBLISH": 0,
|
||||
"BROADCAST": 1,
|
||||
"UNSUBSCRIBE": 2,
|
||||
"DISCONNECT": 3,
|
||||
"PRESENCE": 4,
|
||||
"PRESENCE_STATS": 5,
|
||||
"HISTORY": 6,
|
||||
"HISTORY_REMOVE": 7,
|
||||
"CHANNELS": 8,
|
||||
"INFO": 9,
|
||||
"RPC": 10,
|
||||
"SUBSCRIBE": 11,
|
||||
"REFRESH": 12,
|
||||
"CONNECTIONS": 14,
|
||||
"UPDATE_USER_STATUS": 15,
|
||||
"GET_USER_STATUS": 16,
|
||||
"DELETE_USER_STATUS": 17,
|
||||
"BLOCK_USER": 18,
|
||||
"UNBLOCK_USER": 19,
|
||||
"REVOKE_TOKEN": 20,
|
||||
"INVALIDATE_USER_TOKENS": 21,
|
||||
"DEVICE_REGISTER": 22,
|
||||
"DEVICE_UPDATE": 23,
|
||||
"DEVICE_REMOVE": 24,
|
||||
"DEVICE_LIST": 25,
|
||||
"DEVICE_TOPIC_LIST": 26,
|
||||
"DEVICE_TOPIC_UPDATE": 27,
|
||||
"USER_TOPIC_LIST": 28,
|
||||
"USER_TOPIC_UPDATE": 29,
|
||||
"SEND_PUSH_NOTIFICATION": 30,
|
||||
"UPDATE_PUSH_STATUS": 31,
|
||||
"CANCEL_PUSH": 32,
|
||||
"MAP_PUBLISH": 33,
|
||||
"MAP_REMOVE": 34,
|
||||
"MAP_READ_STATE": 35,
|
||||
"MAP_READ_STREAM": 36,
|
||||
"MAP_STATS": 37,
|
||||
"MAP_CLEAR": 38,
|
||||
"SHARED_POLL_PUBLISH": 39,
|
||||
}
|
||||
|
||||
var (
|
||||
Command_MethodType_name = map[int32]string{
|
||||
0: "PUBLISH",
|
||||
1: "BROADCAST",
|
||||
2: "UNSUBSCRIBE",
|
||||
3: "DISCONNECT",
|
||||
4: "PRESENCE",
|
||||
5: "PRESENCE_STATS",
|
||||
6: "HISTORY",
|
||||
7: "HISTORY_REMOVE",
|
||||
8: "CHANNELS",
|
||||
9: "INFO",
|
||||
10: "RPC",
|
||||
11: "SUBSCRIBE",
|
||||
12: "REFRESH",
|
||||
14: "CONNECTIONS",
|
||||
15: "UPDATE_USER_STATUS",
|
||||
16: "GET_USER_STATUS",
|
||||
17: "DELETE_USER_STATUS",
|
||||
18: "BLOCK_USER",
|
||||
19: "UNBLOCK_USER",
|
||||
20: "REVOKE_TOKEN",
|
||||
21: "INVALIDATE_USER_TOKENS",
|
||||
22: "DEVICE_REGISTER",
|
||||
23: "DEVICE_UPDATE",
|
||||
24: "DEVICE_REMOVE",
|
||||
25: "DEVICE_LIST",
|
||||
26: "DEVICE_TOPIC_LIST",
|
||||
27: "DEVICE_TOPIC_UPDATE",
|
||||
28: "USER_TOPIC_LIST",
|
||||
29: "USER_TOPIC_UPDATE",
|
||||
30: "SEND_PUSH_NOTIFICATION",
|
||||
31: "UPDATE_PUSH_STATUS",
|
||||
32: "CANCEL_PUSH",
|
||||
33: "MAP_PUBLISH",
|
||||
34: "MAP_REMOVE",
|
||||
35: "MAP_READ_STATE",
|
||||
36: "MAP_READ_STREAM",
|
||||
37: "MAP_STATS",
|
||||
38: "MAP_CLEAR",
|
||||
39: "SHARED_POLL_PUBLISH",
|
||||
}
|
||||
)
|
||||
|
||||
func (s *Handler) handleAPICommand(ctx context.Context, cmd *LegacyCommand) (*LegacyReply, error) {
|
||||
|
||||
method := cmd.Method
|
||||
params := cmd.Params
|
||||
|
||||
rep := &LegacyReply{
|
||||
Id: cmd.Id,
|
||||
}
|
||||
|
||||
var replyRes apiproto.Raw
|
||||
|
||||
decoder := GetParamsDecoder()
|
||||
defer PutParamsDecoder(decoder)
|
||||
|
||||
encoder := GetResultEncoder()
|
||||
defer PutResultEncoder(encoder)
|
||||
|
||||
switch method {
|
||||
case Command_PUBLISH:
|
||||
cmd, err := decoder.DecodePublish(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding publish params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Publish(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodePublish(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_BROADCAST:
|
||||
cmd, err := decoder.DecodeBroadcast(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding broadcast params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Broadcast(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeBroadcast(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_SUBSCRIBE:
|
||||
cmd, err := decoder.DecodeSubscribe(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding subscribe params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Subscribe(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeSubscribe(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_UNSUBSCRIBE:
|
||||
cmd, err := decoder.DecodeUnsubscribe(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding unsubscribe params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Unsubscribe(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeUnsubscribe(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_DISCONNECT:
|
||||
cmd, err := decoder.DecodeDisconnect(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding disconnect params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Disconnect(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeDisconnect(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_PRESENCE:
|
||||
cmd, err := decoder.DecodePresence(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding presence params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Presence(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodePresence(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_PRESENCE_STATS:
|
||||
cmd, err := decoder.DecodePresenceStats(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding presence stats params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.PresenceStats(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodePresenceStats(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_HISTORY:
|
||||
cmd, err := decoder.DecodeHistory(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding history params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.History(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeHistory(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_HISTORY_REMOVE:
|
||||
cmd, err := decoder.DecodeHistoryRemove(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding history remove params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.HistoryRemove(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeHistoryRemove(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_INFO:
|
||||
resp := s.api.Info(ctx, &apiproto.InfoRequest{})
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
var err error
|
||||
replyRes, err = encoder.EncodeInfo(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_RPC:
|
||||
cmd, err := decoder.DecodeRPC(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding rpc params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.RPC(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeRPC(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_REFRESH:
|
||||
cmd, err := decoder.DecodeRefresh(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding refresh params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Refresh(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeRefresh(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_CHANNELS:
|
||||
cmd, err := decoder.DecodeChannels(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding channels params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.Channels(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeChannels(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_MAP_PUBLISH:
|
||||
cmd, err := decoder.DecodeMapPublish(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding map publish params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.MapPublish(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeMapPublish(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_MAP_REMOVE:
|
||||
cmd, err := decoder.DecodeMapRemove(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding map remove params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.MapRemove(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeMapRemove(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_MAP_READ_STATE:
|
||||
cmd, err := decoder.DecodeMapReadState(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding map read state params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.MapReadState(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeMapReadState(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_MAP_READ_STREAM:
|
||||
cmd, err := decoder.DecodeMapReadStream(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding map read stream params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.MapReadStream(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeMapReadStream(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_MAP_STATS:
|
||||
cmd, err := decoder.DecodeMapStats(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding map stats params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.MapStats(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeMapStats(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_MAP_CLEAR:
|
||||
cmd, err := decoder.DecodeMapClear(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding map clear params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.MapClear(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeMapClear(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case Command_SHARED_POLL_PUBLISH:
|
||||
cmd, err := decoder.DecodeSharedPollPublish(params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding shared poll publish params")
|
||||
rep.Error = apiproto.ErrorBadRequest
|
||||
return rep, nil
|
||||
}
|
||||
resp := s.api.SharedPollPublish(ctx, cmd)
|
||||
if resp.Error != nil {
|
||||
rep.Error = resp.Error
|
||||
} else {
|
||||
if resp.Result != nil {
|
||||
replyRes, err = encoder.EncodeSharedPollPublish(resp.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
rep.Error = apiproto.ErrorNotFound
|
||||
}
|
||||
|
||||
if replyRes != nil {
|
||||
rep.Result = replyRes
|
||||
}
|
||||
|
||||
return rep, nil
|
||||
}
|
||||
106
internal/api/handler_test.go
Normal file
106
internal/api/handler_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIHandler(t *testing.T) {
|
||||
n := nodeWithMemoryEngine()
|
||||
defer func() { _ = n.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
apiExecutor := NewExecutor(n, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
|
||||
|
||||
mux := http.NewServeMux()
|
||||
apiHandler := NewHandler(n, apiExecutor, Config{})
|
||||
mux.Handle("/api", apiHandler.OldRoute())
|
||||
for path, handler := range apiHandler.Routes() {
|
||||
handlePath := "/api" + path
|
||||
mux.Handle(handlePath, handler)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
// nil body.
|
||||
req, _ := http.NewRequest("POST", server.URL+"/api", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.StatusCode, http.StatusBadRequest)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
// empty body.
|
||||
req, _ = http.NewRequest("POST", server.URL+"/api", strings.NewReader(""))
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.StatusCode, http.StatusBadRequest)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
// valid JSON request.
|
||||
data := `{"method":"publish","params":{"channel": "test", "data":{}}}`
|
||||
req, _ = http.NewRequest("POST", server.URL+"/api", bytes.NewBuffer([]byte(data)))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
// valid JSON request to special method route
|
||||
data = `{"channel": "test", "data":{}}`
|
||||
req, _ = http.NewRequest("POST", server.URL+"/api/publish", bytes.NewBuffer([]byte(data)))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
// request with unknown method.
|
||||
data = `{"method":"unknown","params":{"channel": "test", "data":{}}}`
|
||||
req, _ = http.NewRequest("POST", server.URL+"/api", bytes.NewBuffer([]byte(data)))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.StatusCode, http.StatusBadRequest)
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
func BenchmarkAPIHandler(b *testing.B) {
|
||||
n := nodeWithMemoryEngine()
|
||||
defer func() { _ = n.Shutdown(context.Background()) }()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Client.Insecure = true
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(b, err)
|
||||
|
||||
handler := NewHandler(n, NewExecutor(n, cfgContainer, nil, ExecutorConfig{
|
||||
Protocol: "http",
|
||||
UseOpenTelemetry: false,
|
||||
}), Config{})
|
||||
|
||||
payload := []byte(`{"method": "publish", "params": {"channel": "index", "data": 1}}`)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
request, _ := http.NewRequest(http.MethodPost, "/api", bytes.NewReader(payload))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.handlePublish(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
b.Fatalf("unexpected status code %d", recorder.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user