Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTencentCaptchaVerificationFailed = infraerrors.BadRequest("TENCENT_CAPTCHA_VERIFICATION_FAILED", "tencent captcha verification failed")
|
||||
ErrTencentCaptchaNotConfigured = infraerrors.ServiceUnavailable("TENCENT_CAPTCHA_NOT_CONFIGURED", "tencent captcha not configured")
|
||||
)
|
||||
|
||||
type TencentCaptchaProof struct {
|
||||
Ticket string
|
||||
Randstr string
|
||||
}
|
||||
|
||||
type TencentCaptchaCredentials struct {
|
||||
AppID uint64
|
||||
AppSecretKey string
|
||||
CloudSecretID string
|
||||
CloudSecretKey string
|
||||
// Endpoint 服务端票据校验接入点,由地域推导,repository 层直接使用
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
const (
|
||||
// TencentCaptchaRegionCN 中国站(cloud.tencent.com);TencentCaptchaRegionINTL 国际站(tencentcloud.com)。
|
||||
// 该值同时决定前端加载的 SDK 脚本与服务端校验接入点,两端必须一致:
|
||||
// 国际站 CaptchaAppId 配国内站 SDK 会被腾讯直接判为「appid 所属地域与实际使用地域不符」。
|
||||
TencentCaptchaRegionCN = "cn"
|
||||
TencentCaptchaRegionINTL = "intl"
|
||||
|
||||
tencentCaptchaEndpointCN = "captcha.tencentcloudapi.com"
|
||||
tencentCaptchaEndpointINTL = "captcha.intl.tencentcloudapi.com"
|
||||
)
|
||||
|
||||
// tencentCaptchaEndpoint 按后台配置的地域返回服务端接入点,未知值回退中国站
|
||||
func tencentCaptchaEndpoint(region string) string {
|
||||
if region == TencentCaptchaRegionINTL {
|
||||
return tencentCaptchaEndpointINTL
|
||||
}
|
||||
return tencentCaptchaEndpointCN
|
||||
}
|
||||
|
||||
// normalizeTencentCaptchaRegion 非法值一律视为中国站
|
||||
func normalizeTencentCaptchaRegion(value string) string {
|
||||
if value == TencentCaptchaRegionINTL {
|
||||
return TencentCaptchaRegionINTL
|
||||
}
|
||||
return TencentCaptchaRegionCN
|
||||
}
|
||||
|
||||
type TencentCaptchaVerifyResponse struct {
|
||||
CaptchaCode int64
|
||||
CaptchaMsg string
|
||||
RequestID string
|
||||
}
|
||||
|
||||
type TencentCaptchaVerifier interface {
|
||||
VerifyTicket(context.Context, TencentCaptchaCredentials, TencentCaptchaProof, string) (*TencentCaptchaVerifyResponse, error)
|
||||
}
|
||||
|
||||
type TencentCaptchaService struct {
|
||||
settingService *SettingService
|
||||
verifier TencentCaptchaVerifier
|
||||
}
|
||||
|
||||
func NewTencentCaptchaService(settingService *SettingService, verifier TencentCaptchaVerifier) *TencentCaptchaService {
|
||||
return &TencentCaptchaService{settingService: settingService, verifier: verifier}
|
||||
}
|
||||
|
||||
func (s *TencentCaptchaService) VerifyTicket(ctx context.Context, ticket, randstr, remoteIP string) error {
|
||||
if s == nil || s.settingService == nil {
|
||||
return ErrTencentCaptchaNotConfigured
|
||||
}
|
||||
providerConfig, err := s.settingService.GetCaptchaProviderConfig(ctx)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf("service.tencent_captcha", "%s", "[TencentCaptcha] failed to read captcha provider settings")
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
config := providerConfig.Tencent
|
||||
if !config.Enabled {
|
||||
return nil
|
||||
}
|
||||
return s.VerifyTicketWithConfig(ctx, config, ticket, randstr, remoteIP)
|
||||
}
|
||||
|
||||
func (s *TencentCaptchaService) VerifyTicketWithConfig(ctx context.Context, config TencentCaptchaConfig, ticket, randstr, remoteIP string) error {
|
||||
credentials, ok := parseTencentCaptchaCredentials(config)
|
||||
if !ok || s.verifier == nil {
|
||||
return ErrTencentCaptchaNotConfigured
|
||||
}
|
||||
|
||||
proof := TencentCaptchaProof{
|
||||
Ticket: strings.TrimSpace(ticket),
|
||||
Randstr: strings.TrimSpace(randstr),
|
||||
}
|
||||
if proof.Ticket == "" || proof.Randstr == "" || strings.HasPrefix(proof.Ticket, "trerror_") {
|
||||
return ErrTencentCaptchaVerificationFailed
|
||||
}
|
||||
|
||||
result, err := s.verifier.VerifyTicket(ctx, credentials, proof, remoteIP)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf(
|
||||
"service.tencent_captcha",
|
||||
"[TencentCaptcha] verification request failed region=%s endpoint=%s error=%v",
|
||||
normalizeTencentCaptchaRegion(config.Region),
|
||||
credentials.Endpoint,
|
||||
err,
|
||||
)
|
||||
return fmt.Errorf("%w: verifier request failed", ErrTencentCaptchaVerificationFailed)
|
||||
}
|
||||
if result == nil || result.CaptchaCode != 1 {
|
||||
if result != nil {
|
||||
logger.LegacyPrintf(
|
||||
"service.tencent_captcha",
|
||||
"[TencentCaptcha] rejected region=%s code=%d message=%q request_id=%q",
|
||||
normalizeTencentCaptchaRegion(config.Region),
|
||||
result.CaptchaCode,
|
||||
result.CaptchaMsg,
|
||||
result.RequestID,
|
||||
)
|
||||
} else {
|
||||
logger.LegacyPrintf(
|
||||
"service.tencent_captcha",
|
||||
"[TencentCaptcha] rejected region=%s empty_response=true",
|
||||
normalizeTencentCaptchaRegion(config.Region),
|
||||
)
|
||||
}
|
||||
return ErrTencentCaptchaVerificationFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTencentCaptchaCredentials(config TencentCaptchaConfig) (TencentCaptchaCredentials, bool) {
|
||||
appID, err := strconv.ParseUint(strings.TrimSpace(config.AppID), 10, 64)
|
||||
if err != nil || appID == 0 {
|
||||
return TencentCaptchaCredentials{}, false
|
||||
}
|
||||
credentials := TencentCaptchaCredentials{
|
||||
AppID: appID,
|
||||
AppSecretKey: strings.TrimSpace(config.AppSecretKey),
|
||||
CloudSecretID: strings.TrimSpace(config.CloudSecretID),
|
||||
CloudSecretKey: strings.TrimSpace(config.CloudSecretKey),
|
||||
Endpoint: tencentCaptchaEndpoint(config.Region),
|
||||
}
|
||||
if credentials.AppSecretKey == "" || credentials.CloudSecretID == "" || credentials.CloudSecretKey == "" {
|
||||
return TencentCaptchaCredentials{}, false
|
||||
}
|
||||
return credentials, true
|
||||
}
|
||||
Reference in New Issue
Block a user