212 lines
6.3 KiB
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"`
|
|
}
|