package config import ( "bytes" "encoding/json" "errors" "io" "os" "path/filepath" "strings" ) const defaultTencentSMSConfigFile = "/run/secrets/voice-runtime/tencent-sms.json" type tencentSMSFile struct { TemplateParams map[string][]string `json:"template_params"` Enabled *bool `json:"enabled"` SecretID string `json:"secret_id"` SecretKey string `json:"secret_key"` SDKAppID string `json:"sdk_app_id"` SignName string `json:"sign_name"` Templates map[string]string `json:"templates"` } // Only API and worker consume SMS configuration. In particular, a mounted // configuration must not enable onboarding in callbacks or other services. func (c *OnboardingConfig) loadTencentSMSFile(serviceName string) error { if serviceName != "api" && serviceName != "worker" { return nil } path := strings.TrimSpace(os.Getenv("TENCENT_SMS_CONFIG_FILE")) explicit := path != "" if !explicit { path = defaultTencentSMSConfigFile // The conventional mount exists only on Unix hosts/containers. Windows // development can still opt in with an explicit absolute file path. if !filepath.IsAbs(path) { return nil } } return c.readTencentSMSFile(path, explicit) } func (c *OnboardingConfig) readTencentSMSFile(path string, required bool) error { if !filepath.IsAbs(path) { return errors.New("TENCENT_SMS_CONFIG_FILE must be an absolute path") } file, err := os.Open(filepath.Clean(path)) if errors.Is(err, os.ErrNotExist) && !required { return nil } if err != nil { return errors.New("Tencent SMS configuration file is unavailable") } defer file.Close() info, err := file.Stat() if err != nil || !info.Mode().IsRegular() || info.Size() > 64*1024 { return errors.New("Tencent SMS configuration must be a regular file of at most 64 KiB") } raw, err := io.ReadAll(io.LimitReader(file, 64*1024+1)) if err != nil || len(raw) > 64*1024 { return errors.New("Tencent SMS configuration file could not be read") } // Accept UTF-8 BOM from Windows editors. Never include raw JSON or parser // errors in startup errors: unknown fields may accidentally contain secrets. raw = bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf}) decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() var in tencentSMSFile if err := decoder.Decode(&in); err != nil || in.Enabled == nil { return errors.New("Tencent SMS configuration must match the JSON template and include enabled") } if err := decoder.Decode(new(any)); err != io.EOF { return errors.New("Tencent SMS configuration must contain exactly one JSON object") } for key, value := range in.Templates { if key != "login" && key != "register" && key != "change_password" && key != "change_phone" { return errors.New("Tencent SMS template names must be login, register, change_password or change_phone") } in.Templates[key] = strings.TrimSpace(value) } c.SMSProvider = "tencent" c.SMSLoginEnabled = *in.Enabled c.TencentSMSSecretID = strings.TrimSpace(in.SecretID) c.TencentSMSSecretKey = strings.TrimSpace(in.SecretKey) c.TencentSMSAppID = strings.TrimSpace(in.SDKAppID) c.TencentSMSSignName = strings.TrimSpace(in.SignName) c.TencentSMSTemplateID = "" c.TencentSMSTemplates = in.Templates c.TencentSMSTemplateParams = in.TemplateParams return nil }