package config import ( "bytes" "encoding/json" "errors" "io" "net/url" "os" "path" "strings" "time" "unicode/utf8" "git.ipao.vip/rogee/go-sip/internal/oss" "git.ipao.vip/rogee/go-sip/internal/tenant" ) // LoadOSSConfig reads only the deployment-owned OSS settings for this // Dispatcher. Credentials are resolved from explicit environment references; // the file, its contents and secrets are never echoed in errors. func LoadOSSConfig(filename, dispatcherID string) (oss.Config, error) { return loadOSSConfig(filename, dispatcherID, false) } // LoadNonprodRealOSSConfig accepts only the approved Beijing HTTPS OSS endpoint. // The old isolated Mock loader remains loopback-only. func LoadNonprodRealOSSConfig(filename, dispatcherID string) (oss.Config, error) { return loadOSSConfig(filename, dispatcherID, true) } func loadOSSConfig(filename, dispatcherID string, nonprodReal bool) (oss.Config, error) { if strings.TrimSpace(filename) == "" || tenant.ValidateDispatcherID(dispatcherID) != nil { return oss.Config{}, errors.New("DISPATCHER_OSS_CONFIG_FILE and canonical Dispatcher ID are required") } file, err := os.Open(filename) if err != nil { return oss.Config{}, errors.New("DISPATCHER_OSS_CONFIG_FILE cannot be opened") } const maxBytes = 64 << 10 raw, readErr := io.ReadAll(io.LimitReader(file, maxBytes+1)) if err := errors.Join(readErr, file.Close()); err != nil || len(raw) > maxBytes || !utf8.Valid(raw) { return oss.Config{}, errors.New("DISPATCHER_OSS_CONFIG_FILE must be readable UTF-8 JSON within 64 KiB") } check := json.NewDecoder(bytes.NewReader(raw)) if err := uniqueJSONKeys(check, 0); err != nil { return oss.Config{}, errors.New("DISPATCHER_OSS_CONFIG_FILE has invalid or repeated fields") } if _, err := check.Token(); !errors.Is(err, io.EOF) { return oss.Config{}, errors.New("DISPATCHER_OSS_CONFIG_FILE must contain one JSON object") } var input struct { DispatcherID string `json:"dispatcher_id"` OSS struct { Endpoint string `json:"endpoint"` Region string `json:"region"` Bucket string `json:"bucket"` ObjectPrefix string `json:"object_prefix"` AccessKeyIDEnv string `json:"access_key_id_env"` AccessKeySecretEnv string `json:"access_key_secret_env"` MaxAssetBytes int64 `json:"max_asset_bytes"` } `json:"oss"` } decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil { return oss.Config{}, errors.New("DISPATCHER_OSS_CONFIG_FILE has invalid fields") } if input.DispatcherID != dispatcherID { return oss.Config{}, errors.New("OSS configuration is not assigned to this Dispatcher") } endpoint, err := url.Parse(input.OSS.Endpoint) if err != nil || endpoint.Scheme != "https" || endpoint.User != nil || endpoint.RawQuery != "" || endpoint.Fragment != "" || (endpoint.Path != "" && endpoint.Path != "/") || endpoint.RawPath != "" || (!nonprodReal && (!localEndpoint(input.OSS.Endpoint, "https") || !localGRPCAddress(endpoint.Host, false))) || (nonprodReal && (endpoint.Host != "oss-cn-beijing.aliyuncs.com" || input.OSS.Region != "cn-beijing")) { return oss.Config{}, errors.New("OSS endpoint does not match the selected isolated Mock or approved Beijing real deployment") } prefix := input.OSS.ObjectPrefix if prefix == "" || strings.TrimSpace(prefix) != prefix || path.IsAbs(prefix) || path.Clean(prefix) != prefix || prefix == "." || prefix == ".." || strings.HasPrefix(prefix, "../") || strings.Contains(prefix, `\`) { return oss.Config{}, errors.New("OSS object prefix must be a relative path without traversal") } key, err := fileCredential("access_key_id_env", input.OSS.AccessKeyIDEnv) if err != nil { return oss.Config{}, err } secret, err := fileCredential("access_key_secret_env", input.OSS.AccessKeySecretEnv) if err != nil { return oss.Config{}, err } configuration := oss.Config{ Endpoint: input.OSS.Endpoint, Region: input.OSS.Region, Bucket: input.OSS.Bucket, KeyPrefix: prefix, AccessKeyID: key, AccessKeySecret: secret, GrantTTL: 15 * time.Minute, MaxAssetBytes: input.OSS.MaxAssetBytes, } if err := configuration.Validate(); err != nil { return oss.Config{}, err } return configuration, nil }