266 lines
8.9 KiB
Go
266 lines
8.9 KiB
Go
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,
|
||
})
|
||
}
|