113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
// Package tencentim contains the server-side Tencent Cloud Chat integration boundary.
|
|
package tencentim
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// UserSigConfig describes the stable inputs required by Tencent Cloud Chat UserSig.
|
|
type UserSigConfig struct {
|
|
// SDKAppID is the application ID assigned by Tencent Cloud Chat.
|
|
SDKAppID int64
|
|
// SecretKey is the application key; it must stay on backend services only.
|
|
SecretKey string
|
|
// TTL controls how long the generated UserSig remains valid.
|
|
TTL time.Duration
|
|
}
|
|
|
|
// UserSigResult is returned by gateway-service to authenticated clients.
|
|
type UserSigResult struct {
|
|
// SDKAppID lets clients initialize the Tencent Cloud Chat SDK.
|
|
SDKAppID int64 `json:"sdk_app_id"`
|
|
// UserID is the Tencent Chat identifier; this project uses decimal user_id.
|
|
UserID string `json:"user_id"`
|
|
// UserSig is the signed credential passed to Tencent Cloud Chat SDK login.
|
|
UserSig string `json:"user_sig"`
|
|
// ExpireAtMS lets clients refresh the credential before Tencent rejects login.
|
|
ExpireAtMS int64 `json:"expire_at_ms"`
|
|
}
|
|
|
|
// GenerateUserSig signs a Tencent Cloud Chat UserSig with the HMAC-SHA256 v2 algorithm.
|
|
func GenerateUserSig(cfg UserSigConfig, userID string, now time.Time) (UserSigResult, error) {
|
|
userID = strings.TrimSpace(userID)
|
|
if cfg.SDKAppID <= 0 || cfg.SecretKey == "" || cfg.TTL <= 0 || userID == "" {
|
|
// UserSig protects paid Tencent resources; incomplete config must fail closed.
|
|
return UserSigResult{}, fmt.Errorf("tencent im usersig config is incomplete")
|
|
}
|
|
|
|
current := now.Unix()
|
|
expire := int64(cfg.TTL.Seconds())
|
|
rawSig := signUserSigPayload(cfg.SecretKey, userID, cfg.SDKAppID, current, expire)
|
|
payload := map[string]any{
|
|
"TLS.ver": "2.0",
|
|
"TLS.identifier": userID,
|
|
"TLS.sdkappid": cfg.SDKAppID,
|
|
"TLS.expire": expire,
|
|
"TLS.time": current,
|
|
"TLS.sig": rawSig,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return UserSigResult{}, err
|
|
}
|
|
|
|
compressed, err := zlibCompress(payloadBytes)
|
|
if err != nil {
|
|
return UserSigResult{}, err
|
|
}
|
|
|
|
return UserSigResult{
|
|
SDKAppID: cfg.SDKAppID,
|
|
UserID: userID,
|
|
UserSig: encodeTencentBase64(compressed),
|
|
ExpireAtMS: now.Add(cfg.TTL).UnixMilli(),
|
|
}, nil
|
|
}
|
|
|
|
func signUserSigPayload(secret string, userID string, sdkAppID int64, current int64, expire int64) string {
|
|
// Tencent v2 signing string is line-based and key names are case-sensitive.
|
|
content := "TLS.identifier:" + userID + "\n" +
|
|
"TLS.sdkappid:" + strconv.FormatInt(sdkAppID, 10) + "\n" +
|
|
"TLS.time:" + strconv.FormatInt(current, 10) + "\n" +
|
|
"TLS.expire:" + strconv.FormatInt(expire, 10) + "\n"
|
|
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = mac.Write([]byte(content))
|
|
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func zlibCompress(payload []byte) ([]byte, error) {
|
|
var buffer bytes.Buffer
|
|
writer := zlib.NewWriter(&buffer)
|
|
if _, err := writer.Write(payload); err != nil {
|
|
_ = writer.Close()
|
|
return nil, err
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
func encodeTencentBase64(payload []byte) string {
|
|
// Tencent UserSig uses standard base64 with URL-hostile characters remapped.
|
|
encoded := base64.StdEncoding.EncodeToString(payload)
|
|
encoded = strings.ReplaceAll(encoded, "+", "*")
|
|
encoded = strings.ReplaceAll(encoded, "/", "-")
|
|
encoded = strings.ReplaceAll(encoded, "=", "_")
|
|
|
|
return encoded
|
|
}
|