90 lines
3.6 KiB
Go
90 lines
3.6 KiB
Go
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"
|
|
)
|
|
|
|
// LoadCurrentOSSConfig 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 LoadCurrentOSSConfig(filename, dispatcherID string) (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 || !localEndpoint(input.OSS.Endpoint, "https") || endpoint.User != nil || endpoint.RawQuery != "" || endpoint.Fragment != "" ||
|
|
(endpoint.Path != "" && endpoint.Path != "/") || endpoint.RawPath != "" || !localGRPCAddress(endpoint.Host, false) {
|
|
return oss.Config{}, errors.New("OSS Mock endpoint must be a local HTTPS service with an explicit port")
|
|
}
|
|
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
|
|
}
|