All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m38s
- Add `WARPBOX_TRUSTED_PROXIES` configuration to restrict accepted forwarded client IP headers to specific proxy IPs/CIDRs, securing client IP resolution. - Integrate `BanService` into the background cleanup job to automatically purge expired abuse and ban evidence events. - Update documentation with reverse proxy security guidelines and a production systemd deployment guide.
312 lines
9.2 KiB
Go
312 lines
9.2 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
AppName string
|
|
AppVersion string
|
|
Environment string
|
|
Addr string
|
|
BaseURL string
|
|
DataDir string
|
|
AdminToken string
|
|
StaticDir string
|
|
TemplateDir string
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
IdleTimeout time.Duration
|
|
TrustedProxies []string
|
|
JobsEnabled bool
|
|
CleanupEnabled bool
|
|
CleanupEvery time.Duration
|
|
ThumbnailEnabled bool
|
|
ThumbnailEvery time.Duration
|
|
MaxUploadSize int64
|
|
DefaultSettings SettingsDefaults
|
|
}
|
|
|
|
type SettingsDefaults struct {
|
|
AnonymousUploadsEnabled bool
|
|
AnonymousMaxUploadMB float64
|
|
AnonymousDailyUploadMB float64
|
|
UserDailyUploadMB float64
|
|
DefaultUserStorageMB float64
|
|
UsageRetentionDays int
|
|
LocalStorageMaxGB float64
|
|
AnonymousMaxDays int
|
|
UserMaxDays int
|
|
AnonymousDailyBoxes int
|
|
UserDailyBoxes int
|
|
AnonymousActiveBoxes int
|
|
UserActiveBoxes int
|
|
ShortWindowRequests int
|
|
ShortWindowSeconds int
|
|
AnonymousStorageBackend string
|
|
UserStorageBackend string
|
|
}
|
|
|
|
func Load() (Config, error) {
|
|
cfg := Config{
|
|
AppName: envString("WARPBOX_APP_NAME", "warpbox.dev"),
|
|
AppVersion: envString("APP_VERSION", "dev"),
|
|
Environment: envString("WARPBOX_ENV", "development"),
|
|
Addr: envString("WARPBOX_ADDR", ":8080"),
|
|
BaseURL: strings.TrimRight(envString("WARPBOX_BASE_URL", "http://localhost:8080"), "/"),
|
|
DataDir: envString("WARPBOX_DATA_DIR", defaultPath("data")),
|
|
AdminToken: envString("WARPBOX_ADMIN_TOKEN", ""),
|
|
StaticDir: envString("WARPBOX_STATIC_DIR", defaultPath("static")),
|
|
TemplateDir: envString("WARPBOX_TEMPLATE_DIR", defaultPath("templates")),
|
|
ReadTimeout: envDuration("WARPBOX_READ_TIMEOUT", 15*time.Second),
|
|
WriteTimeout: envDuration("WARPBOX_WRITE_TIMEOUT", 60*time.Second),
|
|
IdleTimeout: envDuration("WARPBOX_IDLE_TIMEOUT", 120*time.Second),
|
|
TrustedProxies: envCSV("WARPBOX_TRUSTED_PROXIES"),
|
|
JobsEnabled: envBool("WARPBOX_JOBS_ENABLED", true),
|
|
CleanupEnabled: envBool("WARPBOX_CLEANUP_ENABLED", true),
|
|
CleanupEvery: envDuration("WARPBOX_CLEANUP_EVERY", time.Hour),
|
|
ThumbnailEnabled: envBool("WARPBOX_THUMBNAIL_ENABLED", true),
|
|
ThumbnailEvery: envDuration("WARPBOX_THUMBNAIL_EVERY", time.Minute),
|
|
MaxUploadSize: envMegabytes("WARPBOX_MAX_UPLOAD_SIZE_MB", 2048), // 2 GiB default.
|
|
DefaultSettings: SettingsDefaults{
|
|
AnonymousUploadsEnabled: envBool("WARPBOX_ANONYMOUS_UPLOADS_ENABLED", true),
|
|
AnonymousMaxUploadMB: envMegabytesLimitFloat("WARPBOX_ANONYMOUS_MAX_UPLOAD_MB", 512),
|
|
AnonymousDailyUploadMB: envMegabytesLimitFloat("WARPBOX_ANONYMOUS_DAILY_UPLOAD_MB", 2048),
|
|
UserDailyUploadMB: envMegabytesLimitFloat("WARPBOX_USER_DAILY_UPLOAD_MB", 8192),
|
|
DefaultUserStorageMB: envMegabytesFloat("WARPBOX_DEFAULT_USER_STORAGE_MB", 51200),
|
|
UsageRetentionDays: envInt("WARPBOX_USAGE_RETENTION_DAYS", 30),
|
|
LocalStorageMaxGB: envGigabytesFloat("WARPBOX_LOCAL_STORAGE_MAX_GB", 100),
|
|
AnonymousMaxDays: envInt("WARPBOX_ANONYMOUS_MAX_DAYS", 30),
|
|
UserMaxDays: envInt("WARPBOX_USER_MAX_DAYS", 90),
|
|
AnonymousDailyBoxes: envInt("WARPBOX_ANONYMOUS_DAILY_BOXES", 100),
|
|
UserDailyBoxes: envInt("WARPBOX_USER_DAILY_BOXES", 250),
|
|
AnonymousActiveBoxes: envInt("WARPBOX_ANONYMOUS_ACTIVE_BOXES", 500),
|
|
UserActiveBoxes: envInt("WARPBOX_USER_ACTIVE_BOXES", 1000),
|
|
ShortWindowRequests: envInt("WARPBOX_SHORT_WINDOW_REQUESTS", 60),
|
|
ShortWindowSeconds: envInt("WARPBOX_SHORT_WINDOW_SECONDS", 60),
|
|
AnonymousStorageBackend: envString("WARPBOX_ANONYMOUS_STORAGE_BACKEND", "local"),
|
|
UserStorageBackend: envString("WARPBOX_USER_STORAGE_BACKEND", "local"),
|
|
},
|
|
}
|
|
|
|
if cfg.BaseURL == "" {
|
|
return Config{}, fmt.Errorf("WARPBOX_BASE_URL cannot be empty")
|
|
}
|
|
if cfg.MaxUploadSize <= 0 {
|
|
return Config{}, fmt.Errorf("WARPBOX_MAX_UPLOAD_SIZE_MB must be positive")
|
|
}
|
|
if !validUnlimitedMegabyteLimit(cfg.DefaultSettings.AnonymousMaxUploadMB) ||
|
|
!validUnlimitedMegabyteLimit(cfg.DefaultSettings.AnonymousDailyUploadMB) ||
|
|
!validUnlimitedMegabyteLimit(cfg.DefaultSettings.UserDailyUploadMB) ||
|
|
cfg.DefaultSettings.DefaultUserStorageMB <= 0 ||
|
|
cfg.DefaultSettings.UsageRetentionDays <= 0 ||
|
|
cfg.DefaultSettings.LocalStorageMaxGB <= 0 ||
|
|
cfg.DefaultSettings.AnonymousMaxDays <= 0 ||
|
|
cfg.DefaultSettings.UserMaxDays <= 0 ||
|
|
cfg.DefaultSettings.AnonymousDailyBoxes <= 0 ||
|
|
cfg.DefaultSettings.UserDailyBoxes <= 0 ||
|
|
cfg.DefaultSettings.AnonymousActiveBoxes <= 0 ||
|
|
cfg.DefaultSettings.UserActiveBoxes <= 0 ||
|
|
cfg.DefaultSettings.ShortWindowRequests <= 0 ||
|
|
cfg.DefaultSettings.ShortWindowSeconds <= 0 {
|
|
return Config{}, fmt.Errorf("upload policy settings must be positive, with -1 allowed for upload MB limits")
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func defaultPath(name string) string {
|
|
candidates := []string{
|
|
filepath.Join("backend", name),
|
|
name,
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
|
return candidate
|
|
}
|
|
}
|
|
|
|
return filepath.Join("backend", name)
|
|
}
|
|
|
|
func envString(key, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envDuration(key string, fallback time.Duration) time.Duration {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
|
|
parsed, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func envBool(key string, fallback bool) bool {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func envInt(key string, fallback int) int {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func envCSV(key string) []string {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(value, ",")
|
|
values := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
if trimmed := strings.TrimSpace(part); trimmed != "" {
|
|
values = append(values, trimmed)
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
func envMegabytes(key string, fallback float64) int64 {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return megabytesToBytes(fallback)
|
|
}
|
|
|
|
parsed, err := parseMegabytes(value)
|
|
if err != nil {
|
|
return megabytesToBytes(fallback)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func envMegabytesFloat(key string, fallback float64) float64 {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := parseMegabytesFloat(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func envMegabytesLimitFloat(key string, fallback float64) float64 {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := parseMegabytesLimitFloat(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func envGigabytesFloat(key string, fallback float64) float64 {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
normalized := strings.TrimSpace(value)
|
|
normalized = strings.TrimSuffix(normalized, "GB")
|
|
normalized = strings.TrimSuffix(normalized, "Gb")
|
|
normalized = strings.TrimSuffix(normalized, "gb")
|
|
normalized = strings.TrimSpace(normalized)
|
|
parsed, err := strconv.ParseFloat(normalized, 64)
|
|
if err != nil || parsed <= 0 {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func parseMegabytes(value string) (int64, error) {
|
|
sizeMB, err := parseMegabytesFloat(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return megabytesToBytes(sizeMB), nil
|
|
}
|
|
|
|
func parseMegabytesFloat(value string) (float64, error) {
|
|
normalized := strings.TrimSpace(value)
|
|
normalized = strings.TrimSuffix(normalized, "MB")
|
|
normalized = strings.TrimSuffix(normalized, "Mb")
|
|
normalized = strings.TrimSuffix(normalized, "mb")
|
|
normalized = strings.TrimSpace(normalized)
|
|
|
|
sizeMB, err := strconv.ParseFloat(normalized, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid megabyte value %q: %w", value, err)
|
|
}
|
|
if sizeMB <= 0 {
|
|
return 0, fmt.Errorf("megabyte value must be positive")
|
|
}
|
|
|
|
return sizeMB, nil
|
|
}
|
|
|
|
func parseMegabytesLimitFloat(value string) (float64, error) {
|
|
sizeMB, err := parseMegabytesFloatAllowNegativeOne(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !validUnlimitedMegabyteLimit(sizeMB) {
|
|
return 0, fmt.Errorf("megabyte value must be positive or -1 for unlimited")
|
|
}
|
|
return sizeMB, nil
|
|
}
|
|
|
|
func parseMegabytesFloatAllowNegativeOne(value string) (float64, error) {
|
|
normalized := strings.TrimSpace(value)
|
|
normalized = strings.TrimSuffix(normalized, "MB")
|
|
normalized = strings.TrimSuffix(normalized, "Mb")
|
|
normalized = strings.TrimSuffix(normalized, "mb")
|
|
normalized = strings.TrimSpace(normalized)
|
|
|
|
sizeMB, err := strconv.ParseFloat(normalized, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid megabyte value %q: %w", value, err)
|
|
}
|
|
return sizeMB, nil
|
|
}
|
|
|
|
func validUnlimitedMegabyteLimit(value float64) bool {
|
|
return value > 0 || value == -1
|
|
}
|
|
|
|
func megabytesToBytes(sizeMB float64) int64 {
|
|
return int64(math.Round(sizeMB * 1024 * 1024))
|
|
}
|