Files
go-sip/internal/config/current_oss.go
T

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
}