package app import ( "bytes" "errors" "fmt" "net" "net/url" "os" "path/filepath" "github.com/pelletier/go-toml/v2" ) type Policy struct { AnonymousUpload bool `json:"anonymous_upload" toml:"anonymous_upload"` AnonymousDownload bool `json:"anonymous_download" toml:"anonymous_download"` MaxFileBytes int64 `json:"max_file_bytes" toml:"max_file_bytes"` MaxBatchBytes int64 `json:"max_batch_bytes" toml:"max_batch_bytes"` MaxFiles int `json:"max_files" toml:"max_files"` DefaultDays int `json:"default_days" toml:"default_days"` MaxInitialDays int `json:"max_initial_days" toml:"max_initial_days"` MaxLifetimeDays int `json:"max_lifetime_days" toml:"max_lifetime_days"` DefaultDownloads int `json:"default_downloads" toml:"default_downloads"` MaxDownloads int `json:"max_downloads" toml:"max_downloads"` RenewalDays int `json:"renewal_days" toml:"renewal_days"` RenewalMode string `json:"renewal_mode" toml:"renewal_mode"` IPDailyBytes int64 `json:"ip_daily_bytes" toml:"ip_daily_bytes"` IPDailyUploads int `json:"ip_daily_uploads" toml:"ip_daily_uploads"` SessionDailyUploads int `json:"session_daily_uploads" toml:"session_daily_uploads"` RequirePassword bool `json:"require_password" toml:"require_password"` } type Config struct { Listen string `toml:"listen"` PublicURL string `toml:"public_url"` AccessURLs []string `toml:"access_urls"` DataDir string `toml:"data_dir"` SecureCookies bool `toml:"secure_cookies"` AllowLANHTTP bool `toml:"allow_lan_http"` Encryption bool `toml:"encryption"` Deduplication bool `toml:"deduplication"` TrustedProxies []string `toml:"trusted_proxies"` DataBudget int64 `toml:"data_budget"` FileBudget int64 `toml:"file_budget"` MinFreeBytes int64 `toml:"min_free_bytes"` AuditRetentionDays int `toml:"audit_retention_days"` AuditMaxRecords int `toml:"audit_max_records"` AuditMaxBytes int64 `toml:"audit_max_bytes"` MaxUploads int `toml:"max_uploads"` MaxDownloads int `toml:"max_downloads"` MaxIPUploads int `toml:"max_ip_uploads"` MaxIPDownloads int `toml:"max_ip_downloads"` MaxRecords int `toml:"max_records"` TransferIdleSeconds int `toml:"transfer_idle_seconds"` MaxTransferSeconds int `toml:"max_transfer_seconds"` TLSCert string `toml:"tls_cert"` TLSKey string `toml:"tls_key"` Policy Policy `toml:"policy"` } func DefaultConfig() Config { return Config{ Listen: "127.0.0.1:8080", PublicURL: "http://localhost:8080", DataDir: "var", Deduplication: true, DataBudget: 10 << 30, FileBudget: 9 << 30, MinFreeBytes: 1 << 30, MaxUploads: 8, MaxDownloads: 16, MaxIPUploads: 2, MaxIPDownloads: 4, MaxRecords: 200000, MaxTransferSeconds: 3600, TransferIdleSeconds: 30, AuditRetentionDays: 30, AuditMaxRecords: 100000, AuditMaxBytes: 32 << 20, Policy: Policy{AnonymousUpload: true, AnonymousDownload: true, MaxFileBytes: 100 << 20, MaxBatchBytes: 500 << 20, MaxFiles: 10, DefaultDays: 1, MaxInitialDays: 7, MaxLifetimeDays: 30, DefaultDownloads: 5, MaxDownloads: 20, RenewalDays: 3, RenewalMode: "add", IPDailyBytes: 1 << 30, IPDailyUploads: 20, SessionDailyUploads: 20}, } } func (p Policy) Validate() error { if p.MaxBatchBytes > 1<<40 || p.IPDailyBytes > 1<<50 || p.MaxFileBytes <= 0 || p.MaxBatchBytes <= 0 || p.MaxFiles < 1 || p.MaxFiles > 1000 || p.MaxFileBytes > p.MaxBatchBytes || p.DefaultDays < 1 || p.DefaultDays > p.MaxInitialDays || p.MaxInitialDays > p.MaxLifetimeDays || p.MaxLifetimeDays > 3650 || p.DefaultDownloads < 1 || p.DefaultDownloads > p.MaxDownloads || p.MaxDownloads > 100000 || p.RenewalDays < 0 || p.RenewalDays > p.MaxLifetimeDays || p.IPDailyBytes < 1 || p.IPDailyUploads < 1 || p.SessionDailyUploads < 1 { return errors.New("策略参数范围错误") } if p.RenewalMode != "add" && p.RenewalMode != "sliding" { return errors.New("renewal_mode 必须为 add 或 sliding") } return nil } func LoadConfig(path string) (Config, error) { c := DefaultConfig() b, e := os.ReadFile(path) if e != nil { return c, e } if e = toml.NewDecoder(bytes.NewReader(b)).DisallowUnknownFields().Decode(&c); e != nil { return c, e } base, e := filepath.Abs(filepath.Dir(path)) if e != nil { return c, e } if !filepath.IsAbs(c.DataDir) { c.DataDir = filepath.Join(base, c.DataDir) } for _, ptr := range []*string{&c.TLSCert, &c.TLSKey} { if *ptr != "" && !filepath.IsAbs(*ptr) { *ptr = filepath.Join(base, *ptr) } } return c, c.Validate() } func (c Config) Validate() error { if e := c.InitialRuntimeSettings().Validate(); e != nil { return e } if e := c.Policy.Validate(); e != nil { return e } primary, e := normalizeAccessURL(c.PublicURL) if e != nil { return e } u, _ := url.Parse(primary) if len(c.AccessURLs) > 16 { return errors.New("额外访问地址最多16个") } for _, raw := range append([]string{c.PublicURL}, c.AccessURLs...) { base, err := normalizeAccessURL(raw) if err != nil { return err } entry, _ := url.Parse(base) ip := net.ParseIP(entry.Hostname()) if entry.Scheme == "http" && ip != nil && ip.IsPrivate() && !ip.IsLoopback() && !c.AllowLANHTTP { return errors.New("内网HTTP入口须显式开启 allow_lan_http") } } if u.Scheme == "https" && !c.SecureCookies { return errors.New("HTTPS主入口须设置 secure_cookies = true") } if u.Scheme == "http" && c.SecureCookies { return errors.New("HTTP主入口须设置 secure_cookies = false;额外HTTPS入口自动使用安全cookie") } listenHost, port, err := net.SplitHostPort(c.Listen) if err != nil || port == "" { return errors.New("listen须包含地址和端口") } // Preserve explicit-address protection for a single local/LAN HTTP site. ip := net.ParseIP(u.Hostname()) if u.Scheme == "http" && (u.Hostname() == "localhost" || ip != nil && (ip.IsPrivate() || ip.IsLoopback())) { bound := net.ParseIP(listenHost) if listenHost != "localhost" && (bound == nil || !bound.IsPrivate() && !bound.IsLoopback()) { return errors.New("内网HTTP须监听具体私有或回环地址") } } if (c.TLSCert == "") != (c.TLSKey == "") { return errors.New("TLS 证书和私钥需一起配置") } if c.FileBudget <= 0 || c.DataBudget-c.FileBudget < 64<<20 || c.MinFreeBytes < 0 || c.MaxUploads < 1 || c.MaxDownloads < 1 || c.MaxIPUploads < 1 || c.MaxIPDownloads < 1 || c.MaxRecords < 1 || c.MaxTransferSeconds < 1 || c.TransferIdleSeconds < 1 { return fmt.Errorf("容量、并发或传输配置非法") } for _, p := range c.TrustedProxies { if _, _, e := net.ParseCIDR(p); e != nil { return fmt.Errorf("非法代理网段: %s", p) } } return nil }