2026-05-12 09:53:20 +08:00

266 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package http
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
"io"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/logx"
"hyapp/pkg/tencentrtc"
"hyapp/pkg/xerr"
)
const (
tencentRTCEventGroupRoom = 1
tencentRTCEventGroupMedia = 2
tencentRTCEventRoomExit = 104
tencentRTCEventStartAudio = 203
tencentRTCEventStopAudio = 204
)
// tencentRTCCallbackBody 覆盖当前语音房需要消费的腾讯 RTC 房间/媒体回调字段。
type tencentRTCCallbackBody struct {
EventGroupID int `json:"EventGroupId"`
EventType int `json:"EventType"`
CallbackTs int64 `json:"CallbackTs"`
EventInfo tencentRTCCallbackEvent `json:"EventInfo"`
}
type tencentRTCCallbackEvent struct {
RoomID json.RawMessage `json:"RoomId"`
UserID string `json:"UserId"`
EventTs int64 `json:"EventTs"`
EventMsTs int64 `json:"EventMsTs"`
Reason int `json:"Reason"`
}
type tencentRTCCallbackResponse struct {
Code int `json:"code"`
Message string `json:"message,omitempty"`
}
// handleTencentRTCCallback 处理腾讯 RTC 服务端事件回调。
func (h *Handler) handleTencentRTCCallback(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost {
writeTencentRTCCallback(writer, http.StatusMethodNotAllowed, 1, "method not allowed")
return
}
payload, err := io.ReadAll(io.LimitReader(request.Body, 1<<20))
if err != nil || len(bytes.TrimSpace(payload)) == 0 {
writeTencentRTCCallback(writer, http.StatusBadRequest, 1, "invalid callback body")
return
}
if !h.tencentRTCSDKAppIDMatched(request) {
writeTencentRTCCallback(writer, http.StatusForbidden, 1, "sdk_app_id mismatch")
return
}
if !tencentrtc.VerifyCallbackSignature(h.tencentRTC.CallbackSignKey, payload, request.Header.Get(tencentrtc.CallbackHeaderSign)) {
// 腾讯 RTC 回调签名使用原始 body配置缺失或签名缺失都必须 fail-closed。
writeTencentRTCCallback(writer, http.StatusForbidden, 1, "callback signature invalid")
return
}
var body tencentRTCCallbackBody
if err := json.Unmarshal(payload, &body); err != nil {
writeTencentRTCCallback(writer, http.StatusBadRequest, 1, "invalid callback body")
return
}
eventType, known := normalizeTencentRTCEventType(body.EventGroupID, body.EventType)
if !known {
// 白名单外事件只做验签和 SDKAppID 校验,避免腾讯新增事件造成重试风暴。
writeTencentRTCCallback(writer, http.StatusOK, 0, "")
return
}
roomID, ok := parseTencentRTCRoomID(body.EventInfo.RoomID)
userID, userOK := parseTencentIMUserID(body.EventInfo.UserID)
eventTimeMS := tencentRTCEventTimeMS(body)
logCtx := logx.With(request.Context(), slog.String("request_id", httpkit.RequestIDFromContext(request.Context())), slog.String("app_code", tencentRTCCallbackAppCode(request)))
if !ok || !userOK || eventTimeMS <= 0 || h.roomClient == nil {
logx.Warn(logCtx, "gateway_rtc_callback_ignored",
slog.String("provider", "tencent"),
slog.Int("event_group", body.EventGroupID),
slog.Int("event_type", body.EventType),
slog.String("room_id", roomID),
slog.String("external_user_id", body.EventInfo.UserID),
slog.String("reason", "input_invalid"),
)
writeTencentRTCCallback(writer, http.StatusOK, 0, "")
return
}
resp, err := h.roomClient.ApplyRTCEvent(request.Context(), &roomv1.ApplyRTCEventRequest{
Meta: &roomv1.RequestMeta{
RequestId: httpkit.RequestIDFromContext(request.Context()),
CommandId: tencentRTCCallbackCommandID(body, roomID, userID, eventTimeMS),
ActorUserId: userID,
RoomId: roomID,
AppCode: tencentRTCCallbackAppCode(request),
GatewayNodeId: "gateway-local",
SessionId: fmt.Sprintf("tencent-rtc-%d", body.EventType),
SentAtMs: time.Now().UnixMilli(),
},
TargetUserId: userID,
EventType: eventType,
EventTimeMs: eventTimeMS,
Reason: tencentRTCCallbackReason(body),
Source: "tencent_rtc_callback",
ExternalEventId: tencentRTCCallbackExternalEventID(body, roomID, userID, eventTimeMS),
})
if err != nil {
if tencentRTCCallbackCanAckBusinessError(err) {
logx.Warn(logCtx, "gateway_rtc_callback_business_ignored",
slog.String("provider", "tencent"),
slog.String("room_id", roomID),
slog.Int64("user_id", userID),
slog.Int("event_group", body.EventGroupID),
slog.Int("event_type", body.EventType),
slog.String("reason", string(xerr.ReasonFromGRPC(err))),
)
writeTencentRTCCallback(writer, http.StatusOK, 0, "")
return
}
logx.Error(logCtx, "gateway_rtc_callback_room_service_error", err,
slog.String("provider", "tencent"),
slog.String("room_id", roomID),
slog.Int64("user_id", userID),
slog.Int("event_group", body.EventGroupID),
slog.Int("event_type", body.EventType),
)
writeTencentRTCCallback(writer, http.StatusInternalServerError, 1, "room service error")
return
}
logx.Info(logCtx, "gateway_rtc_callback_accepted",
slog.String("provider", "tencent"),
slog.String("room_id", roomID),
slog.Int64("user_id", userID),
slog.Int("event_group", body.EventGroupID),
slog.Int("event_type", body.EventType),
slog.Int64("room_version", resp.GetResult().GetRoomVersion()),
slog.Bool("applied", resp.GetResult().GetApplied()),
)
writeTencentRTCCallback(writer, http.StatusOK, 0, "")
}
func (h *Handler) tencentRTCSDKAppIDMatched(request *http.Request) bool {
if h.tencentRTC.SDKAppID <= 0 {
return false
}
raw := strings.TrimSpace(request.Header.Get(tencentrtc.CallbackHeaderSDKAppID))
value, err := strconv.ParseInt(raw, 10, 64)
return err == nil && value == h.tencentRTC.SDKAppID
}
func normalizeTencentRTCEventType(groupID int, eventType int) (string, bool) {
switch {
case groupID == tencentRTCEventGroupMedia && eventType == tencentRTCEventStartAudio:
return "audio_started", true
case groupID == tencentRTCEventGroupMedia && eventType == tencentRTCEventStopAudio:
return "audio_stopped", true
case groupID == tencentRTCEventGroupRoom && eventType == tencentRTCEventRoomExit:
return "room_exited", true
default:
return "", false
}
}
func parseTencentRTCRoomID(raw json.RawMessage) (string, bool) {
raw = bytes.TrimSpace(raw)
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
return "", false
}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
var value any
if err := decoder.Decode(&value); err != nil {
return "", false
}
switch typed := value.(type) {
case string:
roomID := strings.TrimSpace(typed)
return roomID, roomID != ""
case json.Number:
roomID := strings.TrimSpace(typed.String())
return roomID, roomID != ""
default:
return "", false
}
}
func tencentRTCEventTimeMS(body tencentRTCCallbackBody) int64 {
if body.EventInfo.EventMsTs > 0 {
return body.EventInfo.EventMsTs
}
if body.EventInfo.EventTs > 0 {
return body.EventInfo.EventTs * 1000
}
return body.CallbackTs
}
func tencentRTCCallbackReason(body tencentRTCCallbackBody) string {
switch body.EventType {
case tencentRTCEventRoomExit:
return fmt.Sprintf("rtc_room_exited:%d", body.EventInfo.Reason)
case tencentRTCEventStopAudio:
return fmt.Sprintf("rtc_audio_stopped:%d", body.EventInfo.Reason)
case tencentRTCEventStartAudio:
return "rtc_audio_started"
default:
return fmt.Sprintf("rtc_event:%d:%d", body.EventGroupID, body.EventType)
}
}
func tencentRTCCallbackCommandID(body tencentRTCCallbackBody, roomID string, userID int64, eventTimeMS int64) string {
return "cmd_" + tencentRTCCallbackDigest(body, roomID, userID, eventTimeMS)
}
func tencentRTCCallbackExternalEventID(body tencentRTCCallbackBody, roomID string, userID int64, eventTimeMS int64) string {
return "rtc_evt_" + tencentRTCCallbackDigest(body, roomID, userID, eventTimeMS)
}
func tencentRTCCallbackDigest(body tencentRTCCallbackBody, roomID string, userID int64, eventTimeMS int64) string {
seed := fmt.Sprintf("%d:%d:%s:%d:%d:%d", body.EventGroupID, body.EventType, roomID, userID, eventTimeMS, body.EventInfo.Reason)
sum := sha256.Sum256([]byte(seed))
return hex.EncodeToString(sum[:16])
}
func tencentRTCCallbackAppCode(request *http.Request) string {
if raw := strings.TrimSpace(request.URL.Query().Get("app_code")); raw != "" {
return appcode.Normalize(raw)
}
return appcode.FromContext(request.Context())
}
func tencentRTCCallbackCanAckBusinessError(err error) bool {
reason := xerr.ReasonFromGRPC(err)
return reason == xerr.NotFound || reason == xerr.InvalidArgument || reason == xerr.RoomClosed || reason == xerr.PermissionDenied
}
func writeTencentRTCCallback(writer http.ResponseWriter, statusCode int, code int, message string) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(statusCode)
_ = json.NewEncoder(writer).Encode(tencentRTCCallbackResponse{
Code: code,
Message: message,
})
}