hyapp-server/pkg/tencentrtc/rest_client.go

212 lines
6.3 KiB
Go

// Package tencentrtc contains Tencent RTC token and server-side room APIs.
package tencentrtc
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
const (
// DefaultRESTEndpoint is the Tencent Cloud API endpoint for TRTC room management.
DefaultRESTEndpoint = "trtc.tencentcloudapi.com"
// DefaultRESTRegion is accepted by TRTC room management APIs and keeps local config explicit.
DefaultRESTRegion = "ap-guangzhou"
trtcServiceName = "trtc"
trtcAPIVersion = "2019-07-22"
removeUserByStrRoomAction = "RemoveUserByStrRoomId"
tencentCloudTC3Algorithm = "TC3-HMAC-SHA256"
tencentCloudTC3RequestType = "tc3_request"
)
// RESTConfig describes the Tencent Cloud API credentials used for RTC room management.
type RESTConfig struct {
Enabled bool
// SDKAppID is the TRTC application ID that owns the room.
SDKAppID int64
// SecretID/SecretKey are Tencent Cloud API credentials with permission on the TRTC SDKAppID.
SecretID string
SecretKey string
// Region is required by Tencent Cloud API even though room IDs remain application-global.
Region string
// Endpoint defaults to trtc.tencentcloudapi.com.
Endpoint string
// HTTPClient lets tests inject a fake transport and lets services bound public network latency.
HTTPClient *http.Client
// Now is injectable so signature tests do not depend on wall clock time.
Now func() time.Time
}
// RESTClient wraps Tencent Cloud API 3.0 calls needed by room-service.
type RESTClient struct {
cfg RESTConfig
httpClient *http.Client
now func() time.Time
}
// NewRESTClient validates the API credential boundary and returns a reusable client.
func NewRESTClient(cfg RESTConfig) (*RESTClient, error) {
cfg.SecretID = strings.TrimSpace(cfg.SecretID)
cfg.SecretKey = strings.TrimSpace(cfg.SecretKey)
cfg.Region = strings.TrimSpace(cfg.Region)
cfg.Endpoint = strings.TrimSpace(cfg.Endpoint)
if cfg.SDKAppID <= 0 || cfg.SecretID == "" || cfg.SecretKey == "" {
return nil, fmt.Errorf("tencent rtc rest config is incomplete")
}
if cfg.Region == "" {
cfg.Region = DefaultRESTRegion
}
if cfg.Endpoint == "" {
cfg.Endpoint = DefaultRESTEndpoint
}
if cfg.HTTPClient == nil {
cfg.HTTPClient = &http.Client{Timeout: 5 * time.Second}
}
if cfg.Now == nil {
cfg.Now = func() time.Time { return time.Now().UTC() }
}
return &RESTClient{cfg: cfg, httpClient: cfg.HTTPClient, now: cfg.Now}, nil
}
// RemoveUserByStrRoomID removes a user from a string-ID TRTC room.
// It only affects the live RTC room; room-service must update Room Cell state before calling this.
func (c *RESTClient) RemoveUserByStrRoomID(ctx context.Context, roomID string, userID int64) error {
roomID = strings.TrimSpace(roomID)
if roomID == "" || userID <= 0 {
return fmt.Errorf("rtc remove user is incomplete")
}
payload := removeUserByStrRoomRequest{
SDKAppID: c.cfg.SDKAppID,
RoomID: roomID,
UserIDs: []string{strconv.FormatInt(userID, 10)},
}
return c.post(ctx, removeUserByStrRoomAction, payload)
}
func (c *RESTClient) post(ctx context.Context, action string, payload any) error {
body, err := json.Marshal(payload)
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+strings.TrimSuffix(c.cfg.Endpoint, "/"), bytes.NewReader(body))
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json; charset=utf-8")
request.Header.Set("Host", c.cfg.Endpoint)
request.Header.Set("X-TC-Action", action)
request.Header.Set("X-TC-Version", trtcAPIVersion)
request.Header.Set("X-TC-Region", c.cfg.Region)
c.sign(request, body, action)
response, err := c.httpClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("tencent rtc http status %d: %s", response.StatusCode, string(responseBody))
}
var cloudResp cloudAPIResponse
if err := json.Unmarshal(responseBody, &cloudResp); err != nil {
return err
}
if cloudResp.Response.Error != nil {
return fmt.Errorf("tencent rtc rest failed: code=%s message=%s", cloudResp.Response.Error.Code, cloudResp.Response.Error.Message)
}
return nil
}
func (c *RESTClient) sign(request *http.Request, body []byte, action string) {
now := c.now().UTC()
timestamp := now.Unix()
date := now.Format("2006-01-02")
request.Header.Set("X-TC-Timestamp", strconv.FormatInt(timestamp, 10))
hashedPayload := sha256Hex(body)
canonicalHeaders := "content-type:" + strings.ToLower(request.Header.Get("Content-Type")) + "\n" + "host:" + c.cfg.Endpoint + "\n"
signedHeaders := "content-type;host"
canonicalRequest := strings.Join([]string{
http.MethodPost,
"/",
"",
canonicalHeaders,
signedHeaders,
hashedPayload,
}, "\n")
credentialScope := date + "/" + trtcServiceName + "/" + tencentCloudTC3RequestType
stringToSign := strings.Join([]string{
tencentCloudTC3Algorithm,
strconv.FormatInt(timestamp, 10),
credentialScope,
sha256Hex([]byte(canonicalRequest)),
}, "\n")
signature := hex.EncodeToString(hmacSHA256(tc3SigningKey(c.cfg.SecretKey, date), stringToSign))
authorization := fmt.Sprintf(
"%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
tencentCloudTC3Algorithm,
c.cfg.SecretID,
credentialScope,
signedHeaders,
signature,
)
request.Header.Set("Authorization", authorization)
_ = action
}
func tc3SigningKey(secretKey string, date string) []byte {
secretDate := hmacSHA256([]byte("TC3"+secretKey), date)
secretService := hmacSHA256(secretDate, trtcServiceName)
return hmacSHA256(secretService, tencentCloudTC3RequestType)
}
func hmacSHA256(key []byte, value string) []byte {
mac := hmac.New(sha256.New, key)
_, _ = mac.Write([]byte(value))
return mac.Sum(nil)
}
func sha256Hex(value []byte) string {
sum := sha256.Sum256(value)
return hex.EncodeToString(sum[:])
}
type removeUserByStrRoomRequest struct {
SDKAppID int64 `json:"SdkAppId"`
RoomID string `json:"RoomId"`
UserIDs []string `json:"UserIds"`
}
type cloudAPIResponse struct {
Response struct {
RequestID string `json:"RequestId"`
Error *cloudAPIError `json:"Error,omitempty"`
} `json:"Response"`
}
type cloudAPIError struct {
Code string `json:"Code"`
Message string `json:"Message"`
}