* feat(gateway): 接入原生 Responses WebSocket 与逐轮治理 * feat(gateway): 增加 WS 分层开关并修复连接治理 * fix(gateway): 关闭停用或删除分组的 WS 连接
631 lines
19 KiB
Go
631 lines
19 KiB
Go
package state
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"net/textproto"
|
|
"strings"
|
|
"time"
|
|
|
|
"gpt-load/internal/parameteroverride"
|
|
"gpt-load/internal/platform/config"
|
|
"gpt-load/internal/platform/httpheader"
|
|
)
|
|
|
|
const (
|
|
SettingFirstByteTimeout = "first_byte_timeout"
|
|
SettingRequestTimeout = "request_timeout"
|
|
SettingStreamIdleTimeout = "stream_idle_timeout"
|
|
SettingHeaderRules = "header_rules"
|
|
SettingCORS = "cors"
|
|
SettingResponseHeaderRules = "response_header_rules"
|
|
SettingRetryCount = "retry_count"
|
|
SettingRouteStrategy = "route_strategy"
|
|
SettingBlacklistThreshold = "blacklist_threshold"
|
|
SettingAffinityEnabled = "affinity_enabled"
|
|
SettingResponsesWebsocketEnabled = "responses_websocket_enabled"
|
|
SettingAffinityTTL = "affinity_ttl"
|
|
SettingAffinityCapacity = "affinity_capacity"
|
|
SettingValidationInterval = "validation_interval"
|
|
SettingRequestLogRetentionDays = "request_log_retention_days"
|
|
SettingModelsDevAutoSyncEnabled = "models_dev_auto_sync_enabled"
|
|
SettingParameterOverrides = "parameter_overrides"
|
|
)
|
|
|
|
type RouteStrategy string
|
|
|
|
const (
|
|
RouteStrategyNativeFirst RouteStrategy = "native_first"
|
|
RouteStrategyWeightedMix RouteStrategy = "weighted_mix"
|
|
)
|
|
|
|
const (
|
|
defaultRequestLogRetentionDays = 7
|
|
minRequestLogRetentionDays = 1
|
|
maxRequestLogRetentionDays = 365
|
|
defaultAffinityCapacity = 10_000
|
|
maxAffinityCapacity = 1_000_000
|
|
maxJSONSafeInteger = int64(1<<53 - 1)
|
|
)
|
|
|
|
type RuntimeSettings struct {
|
|
FirstByteTimeout time.Duration
|
|
RequestTimeout time.Duration
|
|
StreamIdleTimeout time.Duration
|
|
HeaderRules HeaderRules
|
|
CORS CORSConfig
|
|
ResponseHeaderRules HeaderRules
|
|
RetryCount int
|
|
RouteStrategy RouteStrategy
|
|
BlacklistThreshold int
|
|
AffinityEnabled bool
|
|
ResponsesWebsocketEnabled bool
|
|
AffinityTTL time.Duration
|
|
AffinityCapacity int
|
|
ValidationInterval time.Duration
|
|
RequestLogRetentionDays int
|
|
ModelsDevAutoSyncEnabled bool
|
|
}
|
|
|
|
type ResolvedGroupSettings struct {
|
|
Timeouts TimeoutConfig
|
|
HeaderRules HeaderRules
|
|
BlacklistThreshold int
|
|
AffinityEnabled bool
|
|
ResponsesWebsocketEnabled bool
|
|
ParameterOverrides parameteroverride.Rules
|
|
}
|
|
|
|
func DefaultRuntimeSettings() RuntimeSettings {
|
|
return RuntimeSettings{
|
|
FirstByteTimeout: 120 * time.Second,
|
|
RequestTimeout: 600 * time.Second,
|
|
StreamIdleTimeout: 300 * time.Second,
|
|
HeaderRules: HeaderRules{Set: map[string]string{}},
|
|
CORS: defaultCORSConfig(),
|
|
ResponseHeaderRules: HeaderRules{Set: map[string]string{}},
|
|
RetryCount: 2,
|
|
RouteStrategy: RouteStrategyNativeFirst,
|
|
BlacklistThreshold: 3,
|
|
AffinityEnabled: true,
|
|
ResponsesWebsocketEnabled: true,
|
|
AffinityTTL: time.Hour,
|
|
AffinityCapacity: defaultAffinityCapacity,
|
|
ValidationInterval: 10 * time.Minute,
|
|
RequestLogRetentionDays: defaultRequestLogRetentionDays,
|
|
ModelsDevAutoSyncEnabled: true,
|
|
}
|
|
}
|
|
|
|
func IsRuntimeSettingKey(key string) bool {
|
|
switch key {
|
|
case SettingFirstByteTimeout,
|
|
SettingRequestTimeout,
|
|
SettingStreamIdleTimeout,
|
|
SettingHeaderRules,
|
|
SettingCORS,
|
|
SettingResponseHeaderRules,
|
|
SettingRetryCount,
|
|
SettingRouteStrategy,
|
|
SettingBlacklistThreshold,
|
|
SettingAffinityEnabled,
|
|
SettingResponsesWebsocketEnabled,
|
|
SettingAffinityTTL,
|
|
SettingAffinityCapacity,
|
|
SettingValidationInterval,
|
|
SettingRequestLogRetentionDays,
|
|
SettingModelsDevAutoSyncEnabled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func ResolveRuntimeSettings(settings config.Settings) (RuntimeSettings, error) {
|
|
resolved := DefaultRuntimeSettings()
|
|
for key, value := range settings {
|
|
switch key {
|
|
case SettingFirstByteTimeout:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.FirstByteTimeout = time.Duration(seconds) * time.Second
|
|
case SettingRequestTimeout:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.RequestTimeout = time.Duration(seconds) * time.Second
|
|
case SettingStreamIdleTimeout:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.StreamIdleTimeout = time.Duration(seconds) * time.Second
|
|
case SettingHeaderRules:
|
|
rules, err := parseHeaderRules(value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.HeaderRules = rules
|
|
case SettingCORS:
|
|
cors, err := parseCORSConfig(value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.CORS = cors
|
|
case SettingResponseHeaderRules:
|
|
rules, err := parseResponseHeaderRules(value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.ResponseHeaderRules = rules
|
|
case SettingRetryCount:
|
|
count, err := nonNegativeWholeNumber(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.RetryCount = count
|
|
case SettingRouteStrategy:
|
|
strategy, err := parseRouteStrategy(value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.RouteStrategy = strategy
|
|
case SettingBlacklistThreshold:
|
|
threshold, err := nonNegativeWholeNumber(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.BlacklistThreshold = threshold
|
|
case SettingAffinityEnabled:
|
|
value, err := strictBoolean(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.AffinityEnabled = value
|
|
case SettingResponsesWebsocketEnabled:
|
|
value, err := strictBoolean(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.ResponsesWebsocketEnabled = value
|
|
case SettingAffinityTTL:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.AffinityTTL = time.Duration(seconds) * time.Second
|
|
case SettingAffinityCapacity:
|
|
capacity, err := wholeNumberInRange(key, value, 1, maxAffinityCapacity)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.AffinityCapacity = capacity
|
|
case SettingValidationInterval:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.ValidationInterval = time.Duration(seconds) * time.Second
|
|
case SettingRequestLogRetentionDays:
|
|
days, err := wholeNumberInRange(
|
|
key,
|
|
value,
|
|
minRequestLogRetentionDays,
|
|
maxRequestLogRetentionDays,
|
|
)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.RequestLogRetentionDays = days
|
|
case SettingModelsDevAutoSyncEnabled:
|
|
value, err := strictBoolean(key, value)
|
|
if err != nil {
|
|
return RuntimeSettings{}, err
|
|
}
|
|
resolved.ModelsDevAutoSyncEnabled = value
|
|
default:
|
|
return RuntimeSettings{}, fmt.Errorf("unknown runtime setting %q", key)
|
|
}
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func ResolveGroupRuntimeSettings(
|
|
base RuntimeSettings,
|
|
settings config.Settings,
|
|
) (ResolvedGroupSettings, error) {
|
|
resolved := ResolvedGroupSettings{
|
|
Timeouts: TimeoutConfig{
|
|
FirstByte: base.FirstByteTimeout,
|
|
Request: base.RequestTimeout,
|
|
StreamIdle: base.StreamIdleTimeout,
|
|
},
|
|
HeaderRules: cloneHeaderRules(base.HeaderRules),
|
|
BlacklistThreshold: base.BlacklistThreshold,
|
|
AffinityEnabled: base.AffinityEnabled,
|
|
ResponsesWebsocketEnabled: base.ResponsesWebsocketEnabled,
|
|
}
|
|
for key, value := range settings {
|
|
switch key {
|
|
case SettingFirstByteTimeout:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.Timeouts.FirstByte = time.Duration(seconds) * time.Second
|
|
case SettingRequestTimeout:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.Timeouts.Request = time.Duration(seconds) * time.Second
|
|
case SettingStreamIdleTimeout:
|
|
seconds, err := positiveWholeSeconds(key, value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.Timeouts.StreamIdle = time.Duration(seconds) * time.Second
|
|
case SettingHeaderRules:
|
|
parsed, err := parseHeaderRules(value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.HeaderRules = parsed
|
|
case SettingRetryCount:
|
|
// 兼容读取历史分组配置;重试预算仅由系统设置决定。
|
|
continue
|
|
case SettingBlacklistThreshold:
|
|
parsed, err := nonNegativeWholeNumber(key, value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.BlacklistThreshold = parsed
|
|
case SettingAffinityEnabled:
|
|
parsed, err := strictBoolean(key, value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.AffinityEnabled = parsed
|
|
case SettingResponsesWebsocketEnabled:
|
|
parsed, err := strictBoolean(key, value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.ResponsesWebsocketEnabled = parsed
|
|
case SettingParameterOverrides:
|
|
parsed, err := parameteroverride.Compile(value)
|
|
if err != nil {
|
|
return ResolvedGroupSettings{}, err
|
|
}
|
|
resolved.ParameterOverrides = parsed
|
|
default:
|
|
return ResolvedGroupSettings{}, fmt.Errorf("unknown group setting %q", key)
|
|
}
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func ValidateRuntimeSetting(key string, value any) error {
|
|
switch key {
|
|
case SettingFirstByteTimeout,
|
|
SettingRequestTimeout,
|
|
SettingStreamIdleTimeout,
|
|
SettingValidationInterval:
|
|
_, err := positiveWholeSeconds(key, value)
|
|
return err
|
|
case SettingHeaderRules:
|
|
_, err := parseHeaderRules(value)
|
|
return err
|
|
case SettingCORS:
|
|
_, err := parseCORSConfig(value)
|
|
return err
|
|
case SettingResponseHeaderRules:
|
|
_, err := parseResponseHeaderRules(value)
|
|
return err
|
|
case SettingRetryCount, SettingBlacklistThreshold:
|
|
_, err := nonNegativeWholeNumber(key, value)
|
|
return err
|
|
case SettingRouteStrategy:
|
|
_, err := parseRouteStrategy(value)
|
|
return err
|
|
case SettingAffinityEnabled, SettingResponsesWebsocketEnabled:
|
|
_, err := strictBoolean(key, value)
|
|
return err
|
|
case SettingAffinityTTL:
|
|
_, err := positiveWholeSeconds(key, value)
|
|
return err
|
|
case SettingAffinityCapacity:
|
|
_, err := wholeNumberInRange(key, value, 1, maxAffinityCapacity)
|
|
return err
|
|
case SettingRequestLogRetentionDays:
|
|
_, err := wholeNumberInRange(
|
|
key,
|
|
value,
|
|
minRequestLogRetentionDays,
|
|
maxRequestLogRetentionDays,
|
|
)
|
|
return err
|
|
case SettingModelsDevAutoSyncEnabled:
|
|
_, err := strictBoolean(key, value)
|
|
return err
|
|
default:
|
|
return fmt.Errorf("unknown runtime setting %q", key)
|
|
}
|
|
}
|
|
|
|
func parseRouteStrategy(value any) (RouteStrategy, error) {
|
|
text, ok := value.(string)
|
|
if ok {
|
|
switch strategy := RouteStrategy(text); strategy {
|
|
case RouteStrategyNativeFirst, RouteStrategyWeightedMix:
|
|
return strategy, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s must be native_first or weighted_mix", SettingRouteStrategy)
|
|
}
|
|
|
|
func strictBoolean(path string, value any) (bool, error) {
|
|
parsed, ok := value.(bool)
|
|
if !ok {
|
|
return false, fmt.Errorf("%s must be a boolean", path)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func nonNegativeWholeNumber(path string, value any) (int, error) {
|
|
var number *big.Int
|
|
switch typed := value.(type) {
|
|
case int:
|
|
number = big.NewInt(int64(typed))
|
|
case int64:
|
|
number = big.NewInt(typed)
|
|
case float64:
|
|
if math.IsNaN(typed) || math.IsInf(typed, 0) {
|
|
return 0, fmt.Errorf("%s must be a non-negative whole number", path)
|
|
}
|
|
parsed := new(big.Rat).SetFloat64(typed)
|
|
if parsed == nil || !parsed.IsInt() {
|
|
return 0, fmt.Errorf("%s must be a non-negative whole number", path)
|
|
}
|
|
number = parsed.Num()
|
|
case json.Number:
|
|
literal := typed.String()
|
|
parsed, ok := new(big.Rat).SetString(literal)
|
|
if !json.Valid([]byte(literal)) || !ok || !parsed.IsInt() {
|
|
return 0, fmt.Errorf("%s must be a non-negative whole number", path)
|
|
}
|
|
number = parsed.Num()
|
|
default:
|
|
return 0, fmt.Errorf("%s must be a non-negative whole number", path)
|
|
}
|
|
// Settings are exposed as JSON numbers and consumed by the management UI.
|
|
// Keep the technical boundary lossless across Go and JavaScript; there is
|
|
// no smaller product-level limit.
|
|
maximum := big.NewInt(maxJSONSafeInteger)
|
|
if number.Sign() < 0 || number.Cmp(maximum) > 0 {
|
|
return 0, fmt.Errorf("%s must be a non-negative whole number within JSON safe integer range", path)
|
|
}
|
|
return int(number.Int64()), nil
|
|
}
|
|
|
|
func wholeNumberInRange(path string, value any, minimum, maximum int) (int, error) {
|
|
var number int64
|
|
switch typed := value.(type) {
|
|
case int:
|
|
number = int64(typed)
|
|
case int64:
|
|
number = typed
|
|
case float64:
|
|
if math.IsNaN(typed) || math.IsInf(typed, 0) || math.Trunc(typed) != typed ||
|
|
typed < float64(minimum) || typed > float64(maximum) {
|
|
return 0, fmt.Errorf("%s must be a whole number between %d and %d", path, minimum, maximum)
|
|
}
|
|
number = int64(typed)
|
|
case json.Number:
|
|
literal := typed.String()
|
|
parsed, ok := new(big.Rat).SetString(literal)
|
|
if !json.Valid([]byte(literal)) || !ok || !parsed.IsInt() || !parsed.Num().IsInt64() {
|
|
return 0, fmt.Errorf("%s must be a whole number between %d and %d", path, minimum, maximum)
|
|
}
|
|
number = parsed.Num().Int64()
|
|
default:
|
|
return 0, fmt.Errorf("%s must be a whole number between %d and %d", path, minimum, maximum)
|
|
}
|
|
if number < int64(minimum) || number > int64(maximum) {
|
|
return 0, fmt.Errorf("%s must be a whole number between %d and %d", path, minimum, maximum)
|
|
}
|
|
return int(number), nil
|
|
}
|
|
|
|
func positiveWholeSeconds(path string, value any) (int64, error) {
|
|
const maxTimeoutSeconds = int64((1<<63)-1) / int64(time.Second)
|
|
|
|
var seconds int64
|
|
switch typed := value.(type) {
|
|
case int:
|
|
seconds = int64(typed)
|
|
case int64:
|
|
seconds = typed
|
|
case float64:
|
|
if math.IsNaN(typed) || math.IsInf(typed, 0) || math.Trunc(typed) != typed || typed > float64(maxTimeoutSeconds) {
|
|
return 0, fmt.Errorf("%s must be a positive whole number", path)
|
|
}
|
|
seconds = int64(typed)
|
|
case json.Number:
|
|
literal := typed.String()
|
|
parsed, ok := new(big.Rat).SetString(literal)
|
|
if !json.Valid([]byte(literal)) || !ok || !parsed.IsInt() || !parsed.Num().IsInt64() {
|
|
return 0, fmt.Errorf("%s must be a positive whole number", path)
|
|
}
|
|
seconds = parsed.Num().Int64()
|
|
default:
|
|
return 0, fmt.Errorf("%s must be a positive whole number", path)
|
|
}
|
|
if seconds <= 0 || seconds > maxTimeoutSeconds {
|
|
return 0, fmt.Errorf("%s must be a positive whole number within duration range", path)
|
|
}
|
|
return seconds, nil
|
|
}
|
|
|
|
func parseHeaderRules(value any) (HeaderRules, error) {
|
|
return parseHeaderRulesWithPolicy(
|
|
value,
|
|
SettingHeaderRules,
|
|
func(name string) bool {
|
|
return httpheader.IsForbiddenRequestRuleName(name) ||
|
|
httpheader.IsCredentialName(name)
|
|
},
|
|
)
|
|
}
|
|
|
|
func parseResponseHeaderRules(value any) (HeaderRules, error) {
|
|
return parseHeaderRulesWithPolicy(
|
|
value,
|
|
SettingResponseHeaderRules,
|
|
httpheader.IsForbiddenResponseRuleName,
|
|
)
|
|
}
|
|
|
|
func parseHeaderRulesWithPolicy(
|
|
value any,
|
|
settingName string,
|
|
forbidden func(string) bool,
|
|
) (HeaderRules, error) {
|
|
rules := HeaderRules{Set: make(map[string]string)}
|
|
object, ok := value.(map[string]any)
|
|
if !ok {
|
|
return HeaderRules{}, fmt.Errorf("%s must be an object", settingName)
|
|
}
|
|
for key := range object {
|
|
if key != "set" && key != "remove" {
|
|
return HeaderRules{}, fmt.Errorf("unknown %s field %q", settingName, key)
|
|
}
|
|
}
|
|
seen := make(map[string]struct{})
|
|
if rawSet, exists := object["set"]; exists {
|
|
set, ok := rawSet.(map[string]any)
|
|
if !ok {
|
|
return HeaderRules{}, fmt.Errorf("%s.set must be an object", settingName)
|
|
}
|
|
for name, rawValue := range set {
|
|
if !validHTTPHeaderName(name) {
|
|
return HeaderRules{}, fmt.Errorf("%s.set contains invalid header name %q", settingName, name)
|
|
}
|
|
canonicalName := textproto.CanonicalMIMEHeaderKey(name)
|
|
if forbidden != nil && forbidden(canonicalName) {
|
|
return HeaderRules{}, fmt.Errorf(
|
|
"%s.set cannot set forbidden header %q",
|
|
settingName,
|
|
canonicalName,
|
|
)
|
|
}
|
|
identity := strings.ToLower(name)
|
|
if _, duplicate := seen[identity]; duplicate {
|
|
return HeaderRules{}, fmt.Errorf(
|
|
"%s.set contains duplicate header %q",
|
|
settingName,
|
|
canonicalName,
|
|
)
|
|
}
|
|
seen[identity] = struct{}{}
|
|
text, ok := rawValue.(string)
|
|
if !ok {
|
|
return HeaderRules{}, fmt.Errorf("%s.set.%s must be a string", settingName, name)
|
|
}
|
|
if !validHTTPHeaderValue(text) {
|
|
return HeaderRules{}, fmt.Errorf("%s.set.%s contains invalid header value", settingName, name)
|
|
}
|
|
rules.Set[canonicalName] = text
|
|
}
|
|
}
|
|
if rawRemove, exists := object["remove"]; exists {
|
|
remove, ok := rawRemove.([]any)
|
|
if !ok {
|
|
return HeaderRules{}, fmt.Errorf("%s.remove must be an array", settingName)
|
|
}
|
|
rules.Remove = make([]string, 0, len(remove))
|
|
for index, rawName := range remove {
|
|
name, ok := rawName.(string)
|
|
if !ok {
|
|
return HeaderRules{}, fmt.Errorf("%s.remove[%d] must be a string", settingName, index)
|
|
}
|
|
if !validHTTPHeaderName(name) {
|
|
return HeaderRules{}, fmt.Errorf(
|
|
"%s.remove[%d] contains invalid header name %q",
|
|
settingName,
|
|
index,
|
|
name,
|
|
)
|
|
}
|
|
canonicalName := textproto.CanonicalMIMEHeaderKey(name)
|
|
if forbidden != nil && forbidden(canonicalName) {
|
|
return HeaderRules{}, fmt.Errorf(
|
|
"%s.remove cannot remove forbidden header %q",
|
|
settingName,
|
|
canonicalName,
|
|
)
|
|
}
|
|
identity := strings.ToLower(name)
|
|
if _, duplicate := seen[identity]; duplicate {
|
|
return HeaderRules{}, fmt.Errorf(
|
|
"%s.remove contains duplicate header %q",
|
|
settingName,
|
|
canonicalName,
|
|
)
|
|
}
|
|
seen[identity] = struct{}{}
|
|
rules.Remove = append(rules.Remove, canonicalName)
|
|
}
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
func cloneHeaderRules(source HeaderRules) HeaderRules {
|
|
cloned := HeaderRules{Set: make(map[string]string, len(source.Set))}
|
|
for name, value := range source.Set {
|
|
cloned.Set[name] = value
|
|
}
|
|
cloned.Remove = append([]string(nil), source.Remove...)
|
|
return cloned
|
|
}
|
|
|
|
func validHTTPHeaderName(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
for index := range len(name) {
|
|
if !isHTTPTokenByte(name[index]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isHTTPTokenByte(value byte) bool {
|
|
switch {
|
|
case value >= '0' && value <= '9':
|
|
return true
|
|
case value >= 'a' && value <= 'z':
|
|
return true
|
|
case value >= 'A' && value <= 'Z':
|
|
return true
|
|
default:
|
|
return strings.IndexByte("!#$%&'*+-.^_`|~", value) >= 0
|
|
}
|
|
}
|
|
|
|
func validHTTPHeaderValue(value string) bool {
|
|
for index := range len(value) {
|
|
character := value[index]
|
|
if (character < ' ' && character != '\t') || character == 0x7f {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|