tencent_captcha_service.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  9. )
  10. var (
  11. ErrTencentCaptchaVerificationFailed = infraerrors.BadRequest("TENCENT_CAPTCHA_VERIFICATION_FAILED", "tencent captcha verification failed")
  12. ErrTencentCaptchaNotConfigured = infraerrors.ServiceUnavailable("TENCENT_CAPTCHA_NOT_CONFIGURED", "tencent captcha not configured")
  13. )
  14. type TencentCaptchaProof struct {
  15. Ticket string
  16. Randstr string
  17. }
  18. type TencentCaptchaCredentials struct {
  19. AppID uint64
  20. AppSecretKey string
  21. CloudSecretID string
  22. CloudSecretKey string
  23. // Endpoint 服务端票据校验接入点,由地域推导,repository 层直接使用
  24. Endpoint string
  25. }
  26. const (
  27. // TencentCaptchaRegionCN 中国站(cloud.tencent.com);TencentCaptchaRegionINTL 国际站(tencentcloud.com)。
  28. // 该值同时决定前端加载的 SDK 脚本与服务端校验接入点,两端必须一致:
  29. // 国际站 CaptchaAppId 配国内站 SDK 会被腾讯直接判为「appid 所属地域与实际使用地域不符」。
  30. TencentCaptchaRegionCN = "cn"
  31. TencentCaptchaRegionINTL = "intl"
  32. tencentCaptchaEndpointCN = "captcha.tencentcloudapi.com"
  33. tencentCaptchaEndpointINTL = "captcha.intl.tencentcloudapi.com"
  34. )
  35. // tencentCaptchaEndpoint 按后台配置的地域返回服务端接入点,未知值回退中国站
  36. func tencentCaptchaEndpoint(region string) string {
  37. if region == TencentCaptchaRegionINTL {
  38. return tencentCaptchaEndpointINTL
  39. }
  40. return tencentCaptchaEndpointCN
  41. }
  42. // normalizeTencentCaptchaRegion 非法值一律视为中国站
  43. func normalizeTencentCaptchaRegion(value string) string {
  44. if value == TencentCaptchaRegionINTL {
  45. return TencentCaptchaRegionINTL
  46. }
  47. return TencentCaptchaRegionCN
  48. }
  49. type TencentCaptchaVerifyResponse struct {
  50. CaptchaCode int64
  51. CaptchaMsg string
  52. RequestID string
  53. }
  54. type TencentCaptchaVerifier interface {
  55. VerifyTicket(context.Context, TencentCaptchaCredentials, TencentCaptchaProof, string) (*TencentCaptchaVerifyResponse, error)
  56. }
  57. type TencentCaptchaService struct {
  58. settingService *SettingService
  59. verifier TencentCaptchaVerifier
  60. }
  61. func NewTencentCaptchaService(settingService *SettingService, verifier TencentCaptchaVerifier) *TencentCaptchaService {
  62. return &TencentCaptchaService{settingService: settingService, verifier: verifier}
  63. }
  64. func (s *TencentCaptchaService) VerifyTicket(ctx context.Context, ticket, randstr, remoteIP string) error {
  65. if s == nil || s.settingService == nil {
  66. return ErrTencentCaptchaNotConfigured
  67. }
  68. providerConfig, err := s.settingService.GetCaptchaProviderConfig(ctx)
  69. if err != nil {
  70. logger.LegacyPrintf("service.tencent_captcha", "%s", "[TencentCaptcha] failed to read captcha provider settings")
  71. return ErrServiceUnavailable
  72. }
  73. config := providerConfig.Tencent
  74. if !config.Enabled {
  75. return nil
  76. }
  77. return s.VerifyTicketWithConfig(ctx, config, ticket, randstr, remoteIP)
  78. }
  79. func (s *TencentCaptchaService) VerifyTicketWithConfig(ctx context.Context, config TencentCaptchaConfig, ticket, randstr, remoteIP string) error {
  80. credentials, ok := parseTencentCaptchaCredentials(config)
  81. if !ok || s.verifier == nil {
  82. return ErrTencentCaptchaNotConfigured
  83. }
  84. proof := TencentCaptchaProof{
  85. Ticket: strings.TrimSpace(ticket),
  86. Randstr: strings.TrimSpace(randstr),
  87. }
  88. if proof.Ticket == "" || proof.Randstr == "" || strings.HasPrefix(proof.Ticket, "trerror_") {
  89. return ErrTencentCaptchaVerificationFailed
  90. }
  91. result, err := s.verifier.VerifyTicket(ctx, credentials, proof, remoteIP)
  92. if err != nil {
  93. logger.LegacyPrintf(
  94. "service.tencent_captcha",
  95. "[TencentCaptcha] verification request failed region=%s endpoint=%s error=%v",
  96. normalizeTencentCaptchaRegion(config.Region),
  97. credentials.Endpoint,
  98. err,
  99. )
  100. return fmt.Errorf("%w: verifier request failed", ErrTencentCaptchaVerificationFailed)
  101. }
  102. if result == nil || result.CaptchaCode != 1 {
  103. if result != nil {
  104. logger.LegacyPrintf(
  105. "service.tencent_captcha",
  106. "[TencentCaptcha] rejected region=%s code=%d message=%q request_id=%q",
  107. normalizeTencentCaptchaRegion(config.Region),
  108. result.CaptchaCode,
  109. result.CaptchaMsg,
  110. result.RequestID,
  111. )
  112. } else {
  113. logger.LegacyPrintf(
  114. "service.tencent_captcha",
  115. "[TencentCaptcha] rejected region=%s empty_response=true",
  116. normalizeTencentCaptchaRegion(config.Region),
  117. )
  118. }
  119. return ErrTencentCaptchaVerificationFailed
  120. }
  121. return nil
  122. }
  123. func parseTencentCaptchaCredentials(config TencentCaptchaConfig) (TencentCaptchaCredentials, bool) {
  124. appID, err := strconv.ParseUint(strings.TrimSpace(config.AppID), 10, 64)
  125. if err != nil || appID == 0 {
  126. return TencentCaptchaCredentials{}, false
  127. }
  128. credentials := TencentCaptchaCredentials{
  129. AppID: appID,
  130. AppSecretKey: strings.TrimSpace(config.AppSecretKey),
  131. CloudSecretID: strings.TrimSpace(config.CloudSecretID),
  132. CloudSecretKey: strings.TrimSpace(config.CloudSecretKey),
  133. Endpoint: tencentCaptchaEndpoint(config.Region),
  134. }
  135. if credentials.AppSecretKey == "" || credentials.CloudSecretID == "" || credentials.CloudSecretKey == "" {
  136. return TencentCaptchaCredentials{}, false
  137. }
  138. return credentials, true
  139. }