Files
gpt-load/internal/control/group_write.go
T

354 lines
9.3 KiB
Go

package control
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/url"
"strings"
"unicode"
"gorm.io/gorm"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/protocol"
"gpt-load/internal/state"
"gpt-load/internal/storage/models"
)
const maxUpstreamKeyLines = 1000
type GroupModel struct {
ID string `json:"id"`
Alias string `json:"alias"`
AliasEnabled bool `json:"-"`
}
func (model *GroupModel) UnmarshalJSON(data []byte) error {
var wire struct {
ID string `json:"id"`
Alias string `json:"alias"`
AliasEnabled bool `json:"alias_enabled"`
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&wire); err != nil {
return fmt.Errorf("decode group model: %w", err)
}
var trailing json.RawMessage
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return fmt.Errorf("decode group model: trailing JSON value")
}
return fmt.Errorf("decode group model trailing value: %w", err)
}
model.ID = wire.ID
model.Alias = wire.Alias
model.AliasEnabled = wire.AliasEnabled
return nil
}
type upstreamKeyCandidate struct {
plaintext string
hash string
}
type normalizedUpstreamKeys struct {
candidates []upstreamKeyCandidate
duplicateLines int
}
type optionalGroupModels struct {
Set bool
Values []GroupModel
}
type optionalField[T any] struct {
Set bool
Null bool
Value T
}
func (field *optionalField[T]) UnmarshalJSON(data []byte) error {
if field == nil {
return fmt.Errorf("optional field receiver is nil")
}
field.Set = true
field.Null = false
var zero T
field.Value = zero
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
field.Null = true
return nil
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
decoder.DisallowUnknownFields()
if err := decoder.Decode(&field.Value); err != nil {
return err
}
var trailing json.RawMessage
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return fmt.Errorf("optional field contains multiple JSON values")
}
return err
}
return nil
}
func (value *optionalGroupModels) UnmarshalJSON(data []byte) error {
if value == nil {
return fmt.Errorf("models must be an array")
}
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
return app_errors.ErrValidation
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
var decoded []GroupModel
if err := decoder.Decode(&decoded); err != nil {
return fmt.Errorf("decode models: %w", err)
}
var trailing json.RawMessage
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return fmt.Errorf("decode models: trailing JSON value")
}
return fmt.Errorf("decode models trailing value: %w", err)
}
value.Set = true
value.Values = decoded
return nil
}
func normalizeUpstreamBaseURL(raw string) (normalized, hostname string, err error) {
parsed, parseErr := url.Parse(strings.TrimSpace(raw))
if parseErr != nil || parsed.Opaque != "" || parsed.Host == "" {
return "", "", app_errors.ErrValidation
}
parsed.Scheme = strings.ToLower(parsed.Scheme)
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", "", app_errors.ErrValidation
}
if parsed.User != nil || parsed.Fragment != "" {
return "", "", app_errors.ErrValidation
}
hostname = strings.ToLower(parsed.Hostname())
if hostname == "" {
return "", "", app_errors.ErrValidation
}
port := parsed.Port()
if port != "" {
parsed.Host = net.JoinHostPort(hostname, port)
} else if strings.Contains(hostname, ":") {
parsed.Host = "[" + hostname + "]"
} else {
parsed.Host = hostname
}
parsed.Path = strings.TrimRight(parsed.Path, "/")
parsed.RawPath = ""
return parsed.String(), hostname, nil
}
func normalizeGroupProtocols(values []protocol.Protocol) ([]protocol.Protocol, error) {
seen := make(map[protocol.Protocol]struct{}, len(values))
for _, value := range values {
if !value.DataPlaneEnabled() {
return nil, app_errors.ErrValidation
}
seen[value] = struct{}{}
}
if len(seen) == 0 {
return nil, app_errors.ErrValidation
}
result := make([]protocol.Protocol, 0, len(seen))
for _, value := range protocol.DataPlaneProtocols() {
if _, exists := seen[value]; exists {
result = append(result, value)
}
}
return result, nil
}
func normalizeGroupModels(values []GroupModel) ([]GroupModel, error) {
result := make([]GroupModel, 0, len(values))
indexesByClientModel := make(map[string][]int, len(values))
clientModelOrder := make([]string, 0, len(values))
for index, value := range values {
normalized := GroupModel{
ID: strings.TrimSpace(value.ID),
}
if normalized.ID == "" {
return nil, app_errors.ErrValidation
}
alias := strings.TrimSpace(value.Alias)
aliasEnabled := value.AliasEnabled
if aliasEnabled {
if alias == "" {
return nil, app_errors.ErrValidation
}
normalized.Alias = alias
}
clientModel := normalized.ID
if normalized.Alias != "" {
clientModel = normalized.Alias
}
if _, exists := indexesByClientModel[clientModel]; !exists {
clientModelOrder = append(clientModelOrder, clientModel)
}
indexesByClientModel[clientModel] = append(indexesByClientModel[clientModel], index)
result = append(result, normalized)
}
conflicts := make([]ModelNameConflict, 0)
for _, clientModel := range clientModelOrder {
indexes := indexesByClientModel[clientModel]
if len(indexes) < 2 {
continue
}
conflicts = append(conflicts, ModelNameConflict{
ClientModel: clientModel,
Indexes: append([]int(nil), indexes...),
})
}
if len(conflicts) > 0 {
return nil, app_errors.NewAPIErrorWithData(
app_errors.ErrModelNameConflict,
ModelNameConflictData{Conflicts: conflicts},
)
}
return result, nil
}
func normalizeGroupName(value *string) (*string, error) {
if value == nil {
return nil, nil
}
normalized := strings.TrimSpace(*value)
if normalized == "" || len([]byte(normalized)) > 255 {
return nil, app_errors.ErrValidation
}
for _, character := range normalized {
if unicode.IsControl(character) {
return nil, app_errors.ErrValidation
}
}
return &normalized, nil
}
func (s *Service) normalizeUpstreamKeys(raw string) (normalizedUpstreamKeys, error) {
result := normalizedUpstreamKeys{
candidates: make([]upstreamKeyCandidate, 0),
}
seen := make(map[string]struct{})
nonEmptyLines := 0
for _, line := range strings.Split(raw, "\n") {
plaintext := strings.TrimSpace(line)
if plaintext == "" {
continue
}
nonEmptyLines++
if nonEmptyLines > maxUpstreamKeyLines {
return normalizedUpstreamKeys{}, app_errors.ErrValidation
}
hash := s.encryption.Hash(plaintext)
if _, duplicate := seen[hash]; duplicate {
result.duplicateLines++
continue
}
seen[hash] = struct{}{}
result.candidates = append(result.candidates, upstreamKeyCandidate{
plaintext: plaintext,
hash: hash,
})
}
if len(result.candidates) == 0 {
return normalizedUpstreamKeys{}, app_errors.ErrValidation
}
return result, nil
}
func (s *Service) persistUpstreamKeys(
tx *gorm.DB,
groupID uint,
normalized normalizedUpstreamKeys,
) ([]state.KeyEntry, int, int, error) {
hashes := make([]string, 0, len(normalized.candidates))
for _, candidate := range normalized.candidates {
hashes = append(hashes, candidate.hash)
}
var existingRows []models.UpstreamKey
if err := tx.Where("group_id = ? AND key_hash IN ?", groupID, hashes).Find(&existingRows).Error; err != nil {
return nil, 0, 0, app_errors.ParseDBError(err)
}
existingByHash := make(map[string]models.UpstreamKey, len(existingRows))
for _, row := range existingRows {
existingByHash[row.KeyHash] = row
}
entries := make([]state.KeyEntry, 0, len(normalized.candidates))
added := 0
duplicated := normalized.duplicateLines
for _, candidate := range normalized.candidates {
row, exists := existingByHash[candidate.hash]
if exists {
duplicated++
} else {
ciphertext, err := s.encryption.Encrypt(candidate.plaintext)
if err != nil {
return nil, 0, 0, fmt.Errorf("encrypt upstream key: %w", err)
}
row = models.UpstreamKey{
GroupID: groupID,
KeyValue: ciphertext,
KeyHash: candidate.hash,
Status: models.UpstreamKeyStatusActive,
}
if err := tx.Create(&row).Error; err != nil {
return nil, 0, 0, app_errors.ParseDBError(err)
}
added++
}
entries = append(entries, state.KeyEntry{
ID: row.ID,
GroupID: row.GroupID,
WeightManual: row.WeightManual,
WeightAuto: state.DefaultWeight,
Status: state.KeyStatus(row.Status),
EncryptedValue: row.KeyValue,
})
}
return entries, added, duplicated, nil
}
func (s *Service) applyMissingRegistryKeys(groupID uint, entries []state.KeyEntry) error {
missing := make([]state.KeyEntry, 0, len(entries))
for _, entry := range entries {
if _, exists := s.registry.EncryptedValue(entry.ID); !exists {
missing = append(missing, entry)
}
}
if len(missing) == 0 {
return nil
}
return s.registry.ApplyImport(groupID, missing)
}
func isLiteralPrivateHost(hostname string) bool {
normalized := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(hostname)), ".")
if normalized == "localhost" {
return true
}
ip := net.ParseIP(strings.Trim(normalized, "[]"))
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() || ip.IsUnspecified()
}