Files
Oandtbphp 6da82242d8 feat(subscription): support custom upstream URLs (#579)
* feat(codex): support custom base URL

* fix(codex): require HTTPS for custom base URL

* docs: document changed subscription functions

* fix(groups): 展示并区分详情中的默认与自定义地址

* fix(groups): 统一上游地址配置交互与图标位置

* fix(subscription): 绑定额度重置操作的上游地址

* fix(gateway): 隔离旧上游请求的健康状态更新

---------

Co-authored-by: tbphp <tangb7420@gmail.com>
2026-09-08 19:44:39 +08:00

113 lines
3.6 KiB
Go

package spec
import (
"encoding/json"
"fmt"
"net/url"
"strings"
)
// NormalizeNonEmpty trims and validates a required text or secret field.
func NormalizeNonEmpty(value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", fmt.Errorf("must not be empty")
}
return value, nil
}
// NormalizeBaseURL canonicalizes a configured HTTP(S) endpoint.
func NormalizeBaseURL(value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", fmt.Errorf("must not be empty")
}
parsed, err := url.Parse(value)
if err != nil || parsed == nil || parsed.Opaque != "" || parsed.User != nil || parsed.Host == "" || parsed.Fragment != "" {
return "", fmt.Errorf("must be an absolute HTTP(S) URL without credentials or fragment")
}
parsed.Scheme = strings.ToLower(parsed.Scheme)
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", fmt.Errorf("must be an absolute HTTP(S) URL without credentials or fragment")
}
if parsed.RawQuery != "" || parsed.ForceQuery {
return "", fmt.Errorf("must not contain query parameters")
}
parsed.Host = strings.ToLower(parsed.Host)
if (parsed.Scheme == "https" && parsed.Port() == "443") ||
(parsed.Scheme == "http" && parsed.Port() == "80") {
hostname := parsed.Hostname()
if strings.Contains(hostname, ":") {
hostname = "[" + hostname + "]"
}
parsed.Host = hostname
}
parsed.Path = strings.TrimRight(parsed.Path, "/")
parsed.RawPath = strings.TrimRight(parsed.RawPath, "/")
parsed.ForceQuery = false
return parsed.String(), nil
}
// NormalizeHTTPSBaseURL canonicalizes an endpoint that will receive sensitive
// credentials and rejects cleartext HTTP targets.
func NormalizeHTTPSBaseURL(value string) (string, error) {
normalized, err := NormalizeBaseURL(value)
if err != nil {
return "", err
}
parsed, err := url.Parse(normalized)
if err != nil || parsed == nil || parsed.Scheme != "https" {
return "", fmt.Errorf("must use HTTPS")
}
return normalized, nil
}
// NormalizeOptionalHTTPSBaseURL 允许清空可选的订阅代理地址。
func NormalizeOptionalHTTPSBaseURL(value string) (string, error) {
if strings.TrimSpace(value) == "" {
return "", nil
}
return NormalizeHTTPSBaseURL(value)
}
// NormalizeCloudIdentifier rejects whitespace and control characters in a
// provider-owned cloud configuration value.
func NormalizeCloudIdentifier(value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" || len(value) > 255 {
return "", fmt.Errorf("must be between 1 and 255 bytes")
}
for _, character := range value {
if character <= ' ' || character == 0x7f {
return "", fmt.Errorf("must not contain whitespace or control characters")
}
}
return value, nil
}
// NormalizeServiceAccountJSON validates and canonicalizes a Google service
// account credential without retaining fields outside the credential object.
func NormalizeServiceAccountJSON(value string) (string, error) {
var object map[string]json.RawMessage
decoder := json.NewDecoder(strings.NewReader(strings.TrimSpace(value)))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&object); err != nil || object == nil {
return "", fmt.Errorf("must be a valid JSON object")
}
for _, key := range []string{"type", "project_id", "client_email", "private_key"} {
raw, exists := object[key]
if !exists {
return "", fmt.Errorf("must contain %s", key)
}
var field string
if json.Unmarshal(raw, &field) != nil || strings.TrimSpace(field) == "" {
return "", fmt.Errorf("must contain non-empty %s", key)
}
}
encoded, err := json.Marshal(object)
if err != nil {
return "", fmt.Errorf("must be a valid JSON object")
}
return string(encoded), nil
}