Files
upstream-centrifugo/internal/api/handler_legacy.go
upstream-mirror b352a9a30d
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s
snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:17:21 +05:00

775 lines
19 KiB
Go

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
}