// Copyright 2026 The frp Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package source import ( "errors" "fmt" "os" "path/filepath" v1 "kanhole/pkg/config/v1" "kanhole/pkg/util/jsonx" ) type StoreSourceConfig struct { Path string `json:"path"` } type storeData struct { Proxies []v1.TypedProxyConfig `json:"proxies,omitempty"` Visitors []v1.TypedVisitorConfig `json:"visitors,omitempty"` } type StoreSource struct { baseSource config StoreSourceConfig } var ( ErrAlreadyExists = errors.New("already exists") ErrNotFound = errors.New("not found") ) const ( storeKindProxy = "proxy" storeKindVisitor = "visitor" ) func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) { if cfg.Path == "" { return nil, fmt.Errorf("path is required") } s := &StoreSource{ baseSource: newBaseSource(), config: cfg, } if err := s.loadFromFile(); err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("failed to load existing data: %w", err) } } return s, nil } func (s *StoreSource) loadFromFile() error { s.mu.Lock() defer s.mu.Unlock() return s.loadFromFileUnlocked() } func (s *StoreSource) loadFromFileUnlocked() error { data, err := os.ReadFile(s.config.Path) if err != nil { return err } type rawStoreData struct { Proxies []jsonx.RawMessage `json:"proxies,omitempty"` Visitors []jsonx.RawMessage `json:"visitors,omitempty"` } stored := rawStoreData{} if err := jsonx.Unmarshal(data, &stored); err != nil { return fmt.Errorf("failed to parse JSON: %w", err) } s.proxies = make(map[string]v1.ProxyConfigurer) s.visitors = make(map[string]v1.VisitorConfigurer) for i, proxyData := range stored.Proxies { proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{ DisallowUnknownFields: false, }) if err != nil { return fmt.Errorf("failed to decode proxy at index %d: %w", i, err) } name := proxyCfg.GetBaseConfig().Name if name == "" { return fmt.Errorf("proxy name cannot be empty") } s.proxies[name] = proxyCfg } for i, visitorData := range stored.Visitors { visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{ DisallowUnknownFields: false, }) if err != nil { return fmt.Errorf("failed to decode visitor at index %d: %w", i, err) } name := visitorCfg.GetBaseConfig().Name if name == "" { return fmt.Errorf("visitor name cannot be empty") } s.visitors[name] = visitorCfg } return nil } func (s *StoreSource) saveToFileUnlocked() error { stored := storeData{ Proxies: make([]v1.TypedProxyConfig, 0, len(s.proxies)), Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)), } for _, p := range s.proxies { stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p}) } for _, v := range s.visitors { stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v}) } data, err := jsonx.MarshalIndent(stored, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } dir := filepath.Dir(s.config.Path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } tmpPath := s.config.Path + ".tmp" f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { return fmt.Errorf("failed to create temp file: %w", err) } if _, err := f.Write(data); err != nil { f.Close() os.Remove(tmpPath) return fmt.Errorf("failed to write temp file: %w", err) } if err := f.Sync(); err != nil { f.Close() os.Remove(tmpPath) return fmt.Errorf("failed to sync temp file: %w", err) } if err := f.Close(); err != nil { os.Remove(tmpPath) return fmt.Errorf("failed to close temp file: %w", err) } if err := os.Rename(tmpPath, s.config.Path); err != nil { os.Remove(tmpPath) return fmt.Errorf("failed to rename temp file: %w", err) } return nil } func (s *StoreSource) persistOrRollbackUnlocked(rollback func()) error { if err := s.saveToFileUnlocked(); err != nil { rollback() return fmt.Errorf("failed to persist: %w", err) } return nil } // Store map selectors return the target map for generic helpers. func proxyStoreEntries(s *StoreSource) map[string]v1.ProxyConfigurer { return s.proxies } func visitorStoreEntries(s *StoreSource) map[string]v1.VisitorConfigurer { return s.visitors } // Store entry helpers share mutation, persistence, and rollback for proxy and visitor maps. // T is intentionally limited by callers to v1.ProxyConfigurer or v1.VisitorConfigurer. func addStoreEntry[T any]( s *StoreSource, entriesFn func(*StoreSource) map[string]T, kind string, name string, value T, ) error { s.mu.Lock() defer s.mu.Unlock() entries := entriesFn(s) if _, exists := entries[name]; exists { return fmt.Errorf("%w: %s %q", ErrAlreadyExists, kind, name) } entries[name] = value return s.persistOrRollbackUnlocked(func() { delete(entries, name) }) } func updateStoreEntry[T any]( s *StoreSource, entriesFn func(*StoreSource) map[string]T, kind string, name string, value T, ) error { s.mu.Lock() defer s.mu.Unlock() entries := entriesFn(s) old, exists := entries[name] if !exists { return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name) } entries[name] = value return s.persistOrRollbackUnlocked(func() { entries[name] = old }) } func removeStoreEntry[T any]( s *StoreSource, entriesFn func(*StoreSource) map[string]T, kind string, name string, ) error { if name == "" { return fmt.Errorf("%s name cannot be empty", kind) } s.mu.Lock() defer s.mu.Unlock() entries := entriesFn(s) old, exists := entries[name] if !exists { return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name) } delete(entries, name) return s.persistOrRollbackUnlocked(func() { entries[name] = old }) } func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error { name, err := validateProxyName(proxy) if err != nil { return err } return addStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy) } func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error { name, err := validateProxyName(proxy) if err != nil { return err } return updateStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy) } func (s *StoreSource) RemoveProxy(name string) error { return removeStoreEntry(s, proxyStoreEntries, storeKindProxy, name) } func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer { s.mu.RLock() defer s.mu.RUnlock() p, exists := s.proxies[name] if !exists { return nil } return p } func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error { name, err := validateVisitorName(visitor) if err != nil { return err } return addStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor) } func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error { name, err := validateVisitorName(visitor) if err != nil { return err } return updateStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor) } func (s *StoreSource) RemoveVisitor(name string) error { return removeStoreEntry(s, visitorStoreEntries, storeKindVisitor, name) } func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer { s.mu.RLock() defer s.mu.RUnlock() v, exists := s.visitors[name] if !exists { return nil } return v } func (s *StoreSource) GetAllProxies() ([]v1.ProxyConfigurer, error) { s.mu.RLock() defer s.mu.RUnlock() result := make([]v1.ProxyConfigurer, 0, len(s.proxies)) for _, p := range s.proxies { result = append(result, p) } return result, nil } func (s *StoreSource) GetAllVisitors() ([]v1.VisitorConfigurer, error) { s.mu.RLock() defer s.mu.RUnlock() result := make([]v1.VisitorConfigurer, 0, len(s.visitors)) for _, v := range s.visitors { result = append(result, v) } return result, nil }