package config import ( "bytes" "encoding/json" "errors" "io" "os" "path/filepath" "strconv" "strings" ) // Credentials are server-only; policy is stored separately in PostgreSQL. type CaptchaClientConfig struct { AppID string `json:"captcha_app_id"` AppSecretKey string `json:"app_secret_key"` } type TencentCaptchaConfig struct { SecretID string `json:"secret_id"` SecretKey string `json:"secret_key"` SaaS CaptchaClientConfig `json:"saas"` Mini CaptchaClientConfig `json:"mini_program"` } func (c *OnboardingConfig) loadTencentCaptchaFile(service string) error { if service != "api" && service != "worker" { return nil } path := strings.TrimSpace(os.Getenv("TENCENT_CAPTCHA_CONFIG_FILE")) required := path != "" if !required { path = "/run/secrets/voice-runtime/tencent-captcha.json" if !filepath.IsAbs(path) { return nil } } if !filepath.IsAbs(path) { return errors.New("TENCENT_CAPTCHA_CONFIG_FILE must be absolute") } f, err := os.Open(filepath.Clean(path)) if errors.Is(err, os.ErrNotExist) && !required { return nil } if err != nil { return errors.New("Tencent CAPTCHA configuration file unavailable") } defer f.Close() info, err := f.Stat() if err != nil || !info.Mode().IsRegular() || info.Size() > 16384 { return errors.New("Tencent CAPTCHA configuration must be a regular file of at most 16 KiB") } raw, err := io.ReadAll(io.LimitReader(f, 16385)) if err != nil || len(raw) > 16384 { return errors.New("Tencent CAPTCHA configuration cannot be read") } d := json.NewDecoder(bytes.NewReader(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf}))) d.DisallowUnknownFields() var in TencentCaptchaConfig if d.Decode(&in) != nil || d.Decode(new(any)) != io.EOF { return errors.New("Tencent CAPTCHA configuration must match the JSON template") } if err = in.Validate(); err != nil { return err } c.TencentCaptcha = in return nil } func (c *TencentCaptchaConfig) Validate() error { c.SecretID = strings.TrimSpace(c.SecretID) c.SecretKey = strings.TrimSpace(c.SecretKey) if (c.SecretID == "") != (c.SecretKey == "") { return errors.New("Tencent CAPTCHA requires both SecretId and SecretKey") } for _, client := range []*CaptchaClientConfig{&c.SaaS, &c.Mini} { client.AppID = strings.TrimSpace(client.AppID) client.AppSecretKey = strings.TrimSpace(client.AppSecretKey) if client.AppID == "" && client.AppSecretKey == "" { continue } id, err := strconv.ParseUint(client.AppID, 10, 64) if err != nil || id == 0 || len(client.AppSecretKey) < 16 || len(client.AppSecretKey) > 256 || c.SecretID == "" { return errors.New("Tencent CAPTCHA client requires numeric CaptchaAppId, AppSecretKey and API credentials") } client.AppID = strconv.FormatUint(id, 10) } if c.SaaS.AppID != "" && c.SaaS.AppID == c.Mini.AppID { return errors.New("SaaS and mini-program CAPTCHA must use different CaptchaAppIds") } return nil }