Build and Push to GHCR / build-and-push (push) Has been cancelled
- Add `allowed_ips` to Link model with IPv4/IPv6 and CIDR support - Validate client IP in proxy auth middleware against link whitelist - Extract client IP from X-Forwarded-For / X-Real-Ip headers - Fix copy button for non-HTTPS contexts via execCommand fallback - Allow editing existing links (name, type, auth mode, rate limit, IPs) - Add dedicated IP whitelist modal for quick editing via [HAPI](https://hapi.run) Co-Authored-By: HAPI <[email protected]>
149 lines
3.3 KiB
Go
149 lines
3.3 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
var configFilePath string = "config.json"
|
|
|
|
func SetConfigFilePath(path string) {
|
|
configFilePath = path
|
|
}
|
|
|
|
func GetConfigFilePath() string {
|
|
return configFilePath
|
|
}
|
|
|
|
type Config struct {
|
|
ListenAddr string `json:"listen_addr"`
|
|
AdminPath string `json:"admin_path"`
|
|
AdminUser string `json:"admin_user"`
|
|
AdminPass string `json:"admin_pass"`
|
|
SessionTimeout int `json:"session_timeout"` // hours
|
|
DockerHubHost string `json:"docker_hub_host"`
|
|
GHCRCacheEnabled bool `json:"ghcr_cache_enabled"`
|
|
CacheDir string `json:"cache_dir"`
|
|
MaxCacheSize int64 `json:"max_cache_size"`
|
|
Links map[string]*Link `json:"links"`
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
type Link struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Token string `json:"token"`
|
|
Type string `json:"type"` // docker, ghcr, github
|
|
AuthMode string `json:"auth_mode"` // single, dual
|
|
Enabled bool `json:"enabled"`
|
|
RateLimit int `json:"rate_limit"` // requests per minute
|
|
AllowedIPs []string `json:"allowed_ips"` // empty = allow all
|
|
CreatedAt int64 `json:"created_at"`
|
|
AccessCount int64 `json:"access_count"`
|
|
LastAccess int64 `json:"last_access"`
|
|
}
|
|
|
|
var (
|
|
cfg *Config
|
|
once sync.Once
|
|
)
|
|
|
|
func Load(path string) (*Config, error) {
|
|
configFilePath = path
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
cfg = defaultConfig()
|
|
return cfg, cfg.Save(path)
|
|
}
|
|
return nil, err
|
|
}
|
|
cfg = defaultConfig()
|
|
if err := json.Unmarshal(data, cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.Links == nil {
|
|
cfg.Links = make(map[string]*Link)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func defaultConfig() *Config {
|
|
return &Config{
|
|
ListenAddr: ":8080",
|
|
AdminPath: "/admin",
|
|
AdminUser: "admin",
|
|
AdminPass: "admin123",
|
|
SessionTimeout: 24,
|
|
DockerHubHost: "registry-1.docker.io",
|
|
GHCRCacheEnabled: true,
|
|
CacheDir: "./cache",
|
|
MaxCacheSize: 10 * 1024 * 1024 * 1024, // 10GB
|
|
Links: make(map[string]*Link),
|
|
}
|
|
}
|
|
|
|
func (c *Config) Save(path string) error {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
dir := filepath.Dir(path)
|
|
if dir != "" && dir != "." {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
data, err := json.MarshalIndent(c, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
func (c *Config) GetLink(id string) (*Link, bool) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
link, ok := c.Links[id]
|
|
return link, ok
|
|
}
|
|
|
|
func (c *Config) GetLinkByToken(token string) (*Link, bool) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
for _, link := range c.Links {
|
|
if link.Token == token {
|
|
return link, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (c *Config) SetLink(link *Link) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.Links[link.ID] = link
|
|
}
|
|
|
|
func (c *Config) DeleteLink(id string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
delete(c.Links, id)
|
|
}
|
|
|
|
func (c *Config) ListLinks() []*Link {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
links := make([]*Link, 0, len(c.Links))
|
|
for _, l := range c.Links {
|
|
links = append(links, l)
|
|
}
|
|
return links
|
|
}
|
|
|
|
func Get() *Config {
|
|
return cfg
|
|
}
|