| | package amp |
| |
|
| | import ( |
| | "context" |
| | "encoding/json" |
| | "fmt" |
| | "os" |
| | "path/filepath" |
| | "strings" |
| | "sync" |
| | "time" |
| |
|
| | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| | log "github.com/sirupsen/logrus" |
| | ) |
| |
|
| | |
| | type SecretSource interface { |
| | Get(ctx context.Context) (string, error) |
| | } |
| |
|
| | |
| | type cachedSecret struct { |
| | value string |
| | expiresAt time.Time |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | type MultiSourceSecret struct { |
| | explicitKey string |
| | envKey string |
| | filePath string |
| | cacheTTL time.Duration |
| |
|
| | mu sync.RWMutex |
| | cache *cachedSecret |
| | } |
| |
|
| | |
| | func NewMultiSourceSecret(explicitKey string, cacheTTL time.Duration) *MultiSourceSecret { |
| | if cacheTTL == 0 { |
| | cacheTTL = 5 * time.Minute |
| | } |
| |
|
| | home, _ := os.UserHomeDir() |
| | filePath := filepath.Join(home, ".local", "share", "amp", "secrets.json") |
| |
|
| | return &MultiSourceSecret{ |
| | explicitKey: strings.TrimSpace(explicitKey), |
| | envKey: "AMP_API_KEY", |
| | filePath: filePath, |
| | cacheTTL: cacheTTL, |
| | } |
| | } |
| |
|
| | |
| | func NewMultiSourceSecretWithPath(explicitKey string, filePath string, cacheTTL time.Duration) *MultiSourceSecret { |
| | if cacheTTL == 0 { |
| | cacheTTL = 5 * time.Minute |
| | } |
| |
|
| | return &MultiSourceSecret{ |
| | explicitKey: strings.TrimSpace(explicitKey), |
| | envKey: "AMP_API_KEY", |
| | filePath: filePath, |
| | cacheTTL: cacheTTL, |
| | } |
| | } |
| |
|
| | |
| | |
| | func (s *MultiSourceSecret) Get(ctx context.Context) (string, error) { |
| | |
| | if s.explicitKey != "" { |
| | return s.explicitKey, nil |
| | } |
| |
|
| | |
| | if envValue := strings.TrimSpace(os.Getenv(s.envKey)); envValue != "" { |
| | return envValue, nil |
| | } |
| |
|
| | |
| | |
| | s.mu.RLock() |
| | if s.cache != nil && time.Now().Before(s.cache.expiresAt) { |
| | value := s.cache.value |
| | s.mu.RUnlock() |
| | return value, nil |
| | } |
| | s.mu.RUnlock() |
| |
|
| | |
| | key, err := s.readFromFile() |
| | if err != nil { |
| | |
| | s.updateCache("") |
| | return "", err |
| | } |
| |
|
| | |
| | s.updateCache(key) |
| | return key, nil |
| | } |
| |
|
| | |
| | func (s *MultiSourceSecret) readFromFile() (string, error) { |
| | content, err := os.ReadFile(s.filePath) |
| | if err != nil { |
| | if os.IsNotExist(err) { |
| | return "", nil |
| | } |
| | return "", fmt.Errorf("failed to read amp secrets from %s: %w", s.filePath, err) |
| | } |
| |
|
| | var secrets map[string]string |
| | if err := json.Unmarshal(content, &secrets); err != nil { |
| | return "", fmt.Errorf("failed to parse amp secrets from %s: %w", s.filePath, err) |
| | } |
| |
|
| | key := strings.TrimSpace(secrets["apiKey@https://ampcode.com/"]) |
| | return key, nil |
| | } |
| |
|
| | |
| | func (s *MultiSourceSecret) updateCache(value string) { |
| | s.mu.Lock() |
| | defer s.mu.Unlock() |
| | s.cache = &cachedSecret{ |
| | value: value, |
| | expiresAt: time.Now().Add(s.cacheTTL), |
| | } |
| | } |
| |
|
| | |
| | func (s *MultiSourceSecret) InvalidateCache() { |
| | s.mu.Lock() |
| | defer s.mu.Unlock() |
| | s.cache = nil |
| | } |
| |
|
| | |
| | func (s *MultiSourceSecret) UpdateExplicitKey(key string) { |
| | if s == nil { |
| | return |
| | } |
| | s.mu.Lock() |
| | s.explicitKey = strings.TrimSpace(key) |
| | s.cache = nil |
| | s.mu.Unlock() |
| | } |
| |
|
| | |
| | type StaticSecretSource struct { |
| | key string |
| | } |
| |
|
| | |
| | func NewStaticSecretSource(key string) *StaticSecretSource { |
| | return &StaticSecretSource{key: strings.TrimSpace(key)} |
| | } |
| |
|
| | |
| | func (s *StaticSecretSource) Get(ctx context.Context) (string, error) { |
| | return s.key, nil |
| | } |
| |
|
| | |
| | |
| | |
| | type MappedSecretSource struct { |
| | defaultSource SecretSource |
| | mu sync.RWMutex |
| | lookup map[string]string |
| | } |
| |
|
| | |
| | func NewMappedSecretSource(defaultSource SecretSource) *MappedSecretSource { |
| | return &MappedSecretSource{ |
| | defaultSource: defaultSource, |
| | lookup: make(map[string]string), |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | func (s *MappedSecretSource) Get(ctx context.Context) (string, error) { |
| | |
| | clientKey := getClientAPIKeyFromContext(ctx) |
| | if clientKey != "" { |
| | s.mu.RLock() |
| | if upstreamKey, ok := s.lookup[clientKey]; ok && upstreamKey != "" { |
| | s.mu.RUnlock() |
| | return upstreamKey, nil |
| | } |
| | s.mu.RUnlock() |
| | } |
| |
|
| | |
| | return s.defaultSource.Get(ctx) |
| | } |
| |
|
| | |
| | |
| | func (s *MappedSecretSource) UpdateMappings(entries []config.AmpUpstreamAPIKeyEntry) { |
| | newLookup := make(map[string]string) |
| |
|
| | for _, entry := range entries { |
| | upstreamKey := strings.TrimSpace(entry.UpstreamAPIKey) |
| | if upstreamKey == "" { |
| | continue |
| | } |
| | for _, clientKey := range entry.APIKeys { |
| | trimmedKey := strings.TrimSpace(clientKey) |
| | if trimmedKey == "" { |
| | continue |
| | } |
| | if _, exists := newLookup[trimmedKey]; exists { |
| | |
| | log.Warnf("amp upstream-api-keys: client API key appears in multiple entries; using first mapping.") |
| | continue |
| | } |
| | newLookup[trimmedKey] = upstreamKey |
| | } |
| | } |
| |
|
| | s.mu.Lock() |
| | s.lookup = newLookup |
| | s.mu.Unlock() |
| | } |
| |
|
| | |
| | func (s *MappedSecretSource) UpdateDefaultExplicitKey(key string) { |
| | if ms, ok := s.defaultSource.(*MultiSourceSecret); ok { |
| | ms.UpdateExplicitKey(key) |
| | } |
| | } |
| |
|
| | |
| | func (s *MappedSecretSource) InvalidateCache() { |
| | if ms, ok := s.defaultSource.(*MultiSourceSecret); ok { |
| | ms.InvalidateCache() |
| | } |
| | } |
| |
|