feat(config): add security feature toggle support

Implements a master toggle for security features across config,
CLI, and application logic. This allows granular control over
whether the advanced security middleware and protections are
active globally.
This commit is contained in:
2026-05-03 23:07:21 +03:00
parent 290a3d15db
commit 43baade930
15 changed files with 78 additions and 12 deletions

View File

@@ -22,6 +22,9 @@ func TestDefaults(t *testing.T) {
if !cfg.GuestUploadsEnabled || !cfg.APIEnabled || !cfg.ZipDownloadsEnabled || !cfg.OneTimeDownloadsEnabled { if !cfg.GuestUploadsEnabled || !cfg.APIEnabled || !cfg.ZipDownloadsEnabled || !cfg.OneTimeDownloadsEnabled {
t.Fatal("expected default guest/API/download toggles to be enabled") t.Fatal("expected default guest/API/download toggles to be enabled")
} }
if !cfg.SecurityEnabled {
t.Fatal("expected security features to be enabled by default")
}
if cfg.AdminUsername != "admin" { if cfg.AdminUsername != "admin" {
t.Fatalf("unexpected admin username: %s", cfg.AdminUsername) t.Fatalf("unexpected admin username: %s", cfg.AdminUsername)
} }
@@ -39,6 +42,7 @@ func TestEnvironmentOverrides(t *testing.T) {
t.Setenv("WARPBOX_BOX_POLL_INTERVAL_MS", "2000") t.Setenv("WARPBOX_BOX_POLL_INTERVAL_MS", "2000")
t.Setenv("WARPBOX_ADMIN_USERNAME", "root") t.Setenv("WARPBOX_ADMIN_USERNAME", "root")
t.Setenv("WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", "true") t.Setenv("WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", "true")
t.Setenv("WARPBOX_SECURITY_ENABLED", "false")
cfg, err := Load() cfg, err := Load()
if err != nil { if err != nil {
@@ -63,6 +67,9 @@ func TestEnvironmentOverrides(t *testing.T) {
if !cfg.OneTimeDownloadRetryOnFailure { if !cfg.OneTimeDownloadRetryOnFailure {
t.Fatal("expected one-time retry-on-failure env override to be applied") t.Fatal("expected one-time retry-on-failure env override to be applied")
} }
if cfg.SecurityEnabled {
t.Fatal("expected security features toggle from environment to be applied")
}
if cfg.Source(SettingAPIEnabled) != SourceEnv { if cfg.Source(SettingAPIEnabled) != SourceEnv {
t.Fatalf("expected API setting source to be env, got %s", cfg.Source(SettingAPIEnabled)) t.Fatalf("expected API setting source to be env, got %s", cfg.Source(SettingAPIEnabled))
} }
@@ -191,6 +198,7 @@ func clearConfigEnv(t *testing.T) {
"WARPBOX_BOX_POLL_INTERVAL_MS", "WARPBOX_BOX_POLL_INTERVAL_MS",
"WARPBOX_THUMBNAIL_BATCH_SIZE", "WARPBOX_THUMBNAIL_BATCH_SIZE",
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS", "WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
"WARPBOX_SECURITY_ENABLED",
} { } {
t.Setenv(name, "") t.Setenv(name, "")
} }

View File

@@ -21,6 +21,7 @@ var Definitions = []SettingDefinition{
{Key: SettingThumbnailBatchSize, EnvName: "WARPBOX_THUMBNAIL_BATCH_SIZE", Label: "Thumbnail batch size", Type: SettingTypeInt, Editable: true, Minimum: 1}, {Key: SettingThumbnailBatchSize, EnvName: "WARPBOX_THUMBNAIL_BATCH_SIZE", Label: "Thumbnail batch size", Type: SettingTypeInt, Editable: true, Minimum: 1},
{Key: SettingThumbnailIntervalSeconds, EnvName: "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", Label: "Thumbnail interval seconds", Type: SettingTypeInt, Editable: true, Minimum: 1}, {Key: SettingThumbnailIntervalSeconds, EnvName: "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", Label: "Thumbnail interval seconds", Type: SettingTypeInt, Editable: true, Minimum: 1},
{Key: SettingActivityRetentionSeconds, EnvName: "WARPBOX_ACTIVITY_RETENTION_SECONDS", Label: "Activity retention seconds", Type: SettingTypeInt64, Editable: true, Minimum: 60}, {Key: SettingActivityRetentionSeconds, EnvName: "WARPBOX_ACTIVITY_RETENTION_SECONDS", Label: "Activity retention seconds", Type: SettingTypeInt64, Editable: true, Minimum: 60},
{Key: SettingSecurityEnabled, EnvName: "WARPBOX_SECURITY_ENABLED", Label: "Security features enabled", Type: SettingTypeBool, Editable: true},
{Key: SettingSecurityIPWhitelist, EnvName: "WARPBOX_SECURITY_IP_WHITELIST", Label: "Security IP whitelist", Type: SettingTypeText, Editable: true}, {Key: SettingSecurityIPWhitelist, EnvName: "WARPBOX_SECURITY_IP_WHITELIST", Label: "Security IP whitelist", Type: SettingTypeText, Editable: true},
{Key: SettingSecurityAdminIPWhitelist, EnvName: "WARPBOX_SECURITY_ADMIN_IP_WHITELIST", Label: "Security admin IP whitelist", Type: SettingTypeText, Editable: true}, {Key: SettingSecurityAdminIPWhitelist, EnvName: "WARPBOX_SECURITY_ADMIN_IP_WHITELIST", Label: "Security admin IP whitelist", Type: SettingTypeText, Editable: true},
{Key: SettingTrustedProxyCIDRs, EnvName: "WARPBOX_TRUSTED_PROXY_CIDRS", Label: "Trusted proxy CIDRs", Type: SettingTypeText, Editable: true}, {Key: SettingTrustedProxyCIDRs, EnvName: "WARPBOX_TRUSTED_PROXY_CIDRS", Label: "Trusted proxy CIDRs", Type: SettingTypeText, Editable: true},

View File

@@ -27,6 +27,7 @@ func Load() (*Config, error) {
ThumbnailBatchSize: 10, ThumbnailBatchSize: 10,
ThumbnailIntervalSeconds: 30, ThumbnailIntervalSeconds: 30,
ActivityRetentionSeconds: 7 * 24 * 60 * 60, ActivityRetentionSeconds: 7 * 24 * 60 * 60,
SecurityEnabled: true,
SecurityLoginWindowSeconds: 10 * 60, SecurityLoginWindowSeconds: 10 * 60,
SecurityLoginMaxAttempts: 8, SecurityLoginMaxAttempts: 8,
SecurityBanSeconds: 30 * 60, SecurityBanSeconds: 30 * 60,
@@ -91,6 +92,7 @@ func Load() (*Config, error) {
{SettingOneTimeDownloadRetryFail, "WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", &cfg.OneTimeDownloadRetryOnFailure}, {SettingOneTimeDownloadRetryFail, "WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", &cfg.OneTimeDownloadRetryOnFailure},
{SettingRenewOnAccessEnabled, "WARPBOX_RENEW_ON_ACCESS_ENABLED", &cfg.RenewOnAccessEnabled}, {SettingRenewOnAccessEnabled, "WARPBOX_RENEW_ON_ACCESS_ENABLED", &cfg.RenewOnAccessEnabled},
{SettingRenewOnDownloadEnabled, "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", &cfg.RenewOnDownloadEnabled}, {SettingRenewOnDownloadEnabled, "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", &cfg.RenewOnDownloadEnabled},
{SettingSecurityEnabled, "WARPBOX_SECURITY_ENABLED", &cfg.SecurityEnabled},
} }
for _, item := range envBools { for _, item := range envBools {
if err := cfg.applyBoolEnv(item.key, item.name, item.target); err != nil { if err := cfg.applyBoolEnv(item.key, item.name, item.target); err != nil {
@@ -209,6 +211,7 @@ func (cfg *Config) captureDefaults() {
cfg.captureDefaultValue(SettingThumbnailBatchSize, strconv.Itoa(cfg.ThumbnailBatchSize)) cfg.captureDefaultValue(SettingThumbnailBatchSize, strconv.Itoa(cfg.ThumbnailBatchSize))
cfg.captureDefaultValue(SettingThumbnailIntervalSeconds, strconv.Itoa(cfg.ThumbnailIntervalSeconds)) cfg.captureDefaultValue(SettingThumbnailIntervalSeconds, strconv.Itoa(cfg.ThumbnailIntervalSeconds))
cfg.captureDefaultValue(SettingActivityRetentionSeconds, strconv.FormatInt(cfg.ActivityRetentionSeconds, 10)) cfg.captureDefaultValue(SettingActivityRetentionSeconds, strconv.FormatInt(cfg.ActivityRetentionSeconds, 10))
cfg.captureDefaultValue(SettingSecurityEnabled, formatBool(cfg.SecurityEnabled))
cfg.captureDefaultValue(SettingSecurityIPWhitelist, cfg.SecurityIPWhitelist) cfg.captureDefaultValue(SettingSecurityIPWhitelist, cfg.SecurityIPWhitelist)
cfg.captureDefaultValue(SettingSecurityAdminIPWhitelist, cfg.SecurityAdminIPWhitelist) cfg.captureDefaultValue(SettingSecurityAdminIPWhitelist, cfg.SecurityAdminIPWhitelist)
cfg.captureDefaultValue(SettingTrustedProxyCIDRs, cfg.TrustedProxyCIDRs) cfg.captureDefaultValue(SettingTrustedProxyCIDRs, cfg.TrustedProxyCIDRs)

View File

@@ -37,6 +37,7 @@ const (
SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds" SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds"
SettingDataDir = "data_dir" SettingDataDir = "data_dir"
SettingActivityRetentionSeconds = "activity_retention_seconds" SettingActivityRetentionSeconds = "activity_retention_seconds"
SettingSecurityEnabled = "security_enabled"
SettingSecurityIPWhitelist = "security_ip_whitelist" SettingSecurityIPWhitelist = "security_ip_whitelist"
SettingSecurityAdminIPWhitelist = "security_admin_ip_whitelist" SettingSecurityAdminIPWhitelist = "security_admin_ip_whitelist"
SettingTrustedProxyCIDRs = "trusted_proxy_cidrs" SettingTrustedProxyCIDRs = "trusted_proxy_cidrs"
@@ -108,6 +109,7 @@ type Config struct {
ThumbnailBatchSize int ThumbnailBatchSize int
ThumbnailIntervalSeconds int ThumbnailIntervalSeconds int
ActivityRetentionSeconds int64 ActivityRetentionSeconds int64
SecurityEnabled bool
SecurityIPWhitelist string SecurityIPWhitelist string
SecurityAdminIPWhitelist string SecurityAdminIPWhitelist string
TrustedProxyCIDRs string TrustedProxyCIDRs string

View File

@@ -95,6 +95,8 @@ func (cfg *Config) assignBool(key string, value bool, source Source) {
cfg.RenewOnAccessEnabled = value cfg.RenewOnAccessEnabled = value
case SettingRenewOnDownloadEnabled: case SettingRenewOnDownloadEnabled:
cfg.RenewOnDownloadEnabled = value cfg.RenewOnDownloadEnabled = value
case SettingSecurityEnabled:
cfg.SecurityEnabled = value
} }
cfg.setValue(key, formatBool(value), source) cfg.setValue(key, formatBool(value), source)
} }

View File

@@ -64,11 +64,11 @@ func (app *App) handleAdminLoginPost(ctx *gin.Context) {
} }
ip := app.clientIP(ctx) ip := app.clientIP(ctx)
guard := app.securityGuard guard := app.securityGuard
if guard == nil { if app.securityFeaturesEnabled() && guard == nil {
guard = security.NewGuard() guard = security.NewGuard()
app.securityGuard = guard app.securityGuard = guard
} }
if !guard.IsAdminWhitelisted(ip) && guard.IsBanned(ip) { if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) && guard.IsBanned(ip) {
app.logActivity("auth.admin.block", "high", "Blocked admin login from banned IP", ctx, nil) app.logActivity("auth.admin.block", "high", "Blocked admin login from banned IP", ctx, nil)
ctx.HTML(http.StatusTooManyRequests, "admin/login.html", gin.H{ ctx.HTML(http.StatusTooManyRequests, "admin/login.html", gin.H{
"ErrorMessage": "Too many failed attempts. Try again later.", "ErrorMessage": "Too many failed attempts. Try again later.",
@@ -80,7 +80,7 @@ func (app *App) handleAdminLoginPost(ctx *gin.Context) {
password := ctx.PostForm("password") password := ctx.PostForm("password")
if username != app.config.AdminUsername || password != app.config.AdminPassword { if username != app.config.AdminUsername || password != app.config.AdminPassword {
if !guard.IsAdminWhitelisted(ip) { if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) {
banned, attempts := guard.RegisterFailedLogin(ip, app.config.SecurityLoginWindowSeconds, app.config.SecurityLoginMaxAttempts, app.config.SecurityBanSeconds) banned, attempts := guard.RegisterFailedLogin(ip, app.config.SecurityLoginWindowSeconds, app.config.SecurityLoginMaxAttempts, app.config.SecurityBanSeconds)
app.logActivity("auth.admin.failed", "medium", "Failed admin login", ctx, map[string]string{"attempts": strconv.Itoa(attempts)}) app.logActivity("auth.admin.failed", "medium", "Failed admin login", ctx, map[string]string{"attempts": strconv.Itoa(attempts)})
if banned { if banned {

View File

@@ -1,6 +1,7 @@
package server package server
import ( import (
"fmt"
"net" "net"
"net/http" "net/http"
"path/filepath" "path/filepath"
@@ -27,14 +28,24 @@ type adminSecurityActionRequest struct {
BanUntil string `json:"ban_until"` BanUntil string `json:"ban_until"`
} }
func (app *App) reloadSecurityConfig() { func (app *App) reloadSecurityConfig() error {
if app == nil || app.config == nil {
return fmt.Errorf("app or config is nil")
}
if !app.securityFeaturesEnabled() {
if app.securityGuard != nil {
_ = app.securityGuard.Close()
}
app.securityGuard = nil
return nil
}
if app.securityGuard == nil { if app.securityGuard == nil {
app.securityGuard = security.NewGuard() app.securityGuard = security.NewGuard()
} }
if app.config != nil { if err := app.securityGuard.EnableBanPersistence(filepath.Join(app.config.DBDir, "bans.badger")); err != nil {
_ = app.securityGuard.EnableBanPersistence(filepath.Join(app.config.DBDir, "bans.badger")) return fmt.Errorf("enable ban persistence: %w", err)
} }
_ = app.securityGuard.Reload(security.Config{ if err := app.securityGuard.Reload(security.Config{
IPWhitelist: app.config.SecurityIPWhitelist, IPWhitelist: app.config.SecurityIPWhitelist,
AdminIPWhitelist: app.config.SecurityAdminIPWhitelist, AdminIPWhitelist: app.config.SecurityAdminIPWhitelist,
LoginWindowSeconds: app.config.SecurityLoginWindowSeconds, LoginWindowSeconds: app.config.SecurityLoginWindowSeconds,
@@ -45,7 +56,14 @@ func (app *App) reloadSecurityConfig() {
UploadWindowSeconds: app.config.SecurityUploadWindowSeconds, UploadWindowSeconds: app.config.SecurityUploadWindowSeconds,
UploadMaxRequests: app.config.SecurityUploadMaxRequests, UploadMaxRequests: app.config.SecurityUploadMaxRequests,
UploadMaxBytes: app.config.SecurityUploadMaxBytes, UploadMaxBytes: app.config.SecurityUploadMaxBytes,
}) }); err != nil {
return fmt.Errorf("reload guard config: %w", err)
}
return nil
}
func (app *App) securityFeaturesEnabled() bool {
return app != nil && app.config != nil && app.config.SecurityEnabled
} }
func (app *App) logActivity(kind string, severity string, message string, ctx *gin.Context, meta map[string]string) { func (app *App) logActivity(kind string, severity string, message string, ctx *gin.Context, meta map[string]string) {
@@ -85,6 +103,10 @@ func (app *App) createAlert(title string, severity string, group string, code st
func (app *App) securityMiddleware() gin.HandlerFunc { func (app *App) securityMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) { return func(ctx *gin.Context) {
if !app.securityFeaturesEnabled() {
ctx.Next()
return
}
if app.securityGuard == nil { if app.securityGuard == nil {
ctx.Next() ctx.Next()
return return
@@ -104,6 +126,10 @@ func (app *App) securityMiddleware() gin.HandlerFunc {
} }
func (app *App) handleNoRoute(ctx *gin.Context) { func (app *App) handleNoRoute(ctx *gin.Context) {
if !app.securityFeaturesEnabled() {
ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
return
}
if app.securityGuard == nil { if app.securityGuard == nil {
ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
return return
@@ -148,6 +174,10 @@ func (app *App) handleAdminActivity(ctx *gin.Context) {
} }
func (app *App) handleAdminSecurity(ctx *gin.Context) { func (app *App) handleAdminSecurity(ctx *gin.Context) {
if !app.securityFeaturesEnabled() {
ctx.String(http.StatusNotFound, "Security features are disabled")
return
}
events := []activity.Event{} events := []activity.Event{}
alertsList := []alerts.Alert{} alertsList := []alerts.Alert{}
if app.activityStore != nil { if app.activityStore != nil {
@@ -215,6 +245,10 @@ func (app *App) recordManualBanAction(ctx *gin.Context, kind string, message str
} }
func (app *App) handleAdminSecurityAction(ctx *gin.Context) { func (app *App) handleAdminSecurityAction(ctx *gin.Context) {
if !app.securityFeaturesEnabled() {
ctx.JSON(http.StatusNotFound, gin.H{"error": "Security features are disabled"})
return
}
if app.securityGuard == nil { if app.securityGuard == nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Security guard unavailable"}) ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Security guard unavailable"})
return return

View File

@@ -111,7 +111,9 @@ func setupAdminSecurityTest(t *testing.T) (*App, *gin.Engine) {
alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")), alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")),
securityGuard: security.NewGuard(), securityGuard: security.NewGuard(),
} }
app.reloadSecurityConfig() if err := app.reloadSecurityConfig(); err != nil {
t.Fatalf("reload security config: %v", err)
}
t.Cleanup(func() { _ = app.securityGuard.Close() }) t.Cleanup(func() { _ = app.securityGuard.Close() })
router := gin.New() router := gin.New()

View File

@@ -269,7 +269,9 @@ func (app *App) applySettingsOverrideSet(values map[string]string) ([]adminSetti
app.config = nextCfg app.config = nextCfg
applyBoxstoreRuntimeConfig(app.config) applyBoxstoreRuntimeConfig(app.config)
app.reloadSecurityConfig() if err := app.reloadSecurityConfig(); err != nil {
return nil, nil, err
}
rows, _ := app.buildAdminSettingsRows() rows, _ := app.buildAdminSettingsRows()
return rows, warnings, nil return rows, warnings, nil
} }
@@ -437,7 +439,7 @@ func settingsCategoryForKey(key string) string {
return "downloads" return "downloads"
case config.SettingRenewOnAccessEnabled, config.SettingDefaultGuestExpirySecs, config.SettingMaxGuestExpirySecs, config.SettingOneTimeDownloadRetryFail: case config.SettingRenewOnAccessEnabled, config.SettingDefaultGuestExpirySecs, config.SettingMaxGuestExpirySecs, config.SettingOneTimeDownloadRetryFail:
return "retention" return "retention"
case config.SettingSecurityIPWhitelist, config.SettingSecurityAdminIPWhitelist, config.SettingSecurityLoginWindowSecs, config.SettingSecurityLoginMaxAttempts, config.SettingSecurityBanSeconds, config.SettingSecurityScanWindowSecs, config.SettingSecurityScanMaxAttempts: case config.SettingSecurityEnabled, config.SettingSecurityIPWhitelist, config.SettingSecurityAdminIPWhitelist, config.SettingSecurityLoginWindowSecs, config.SettingSecurityLoginMaxAttempts, config.SettingSecurityBanSeconds, config.SettingSecurityScanWindowSecs, config.SettingSecurityScanMaxAttempts:
return "security" return "security"
case config.SettingActivityRetentionSeconds: case config.SettingActivityRetentionSeconds:
return "activity" return "activity"
@@ -476,6 +478,7 @@ func settingsDescription(key string) string {
config.SettingThumbnailIntervalSeconds: "Delay between thumbnail worker passes.", config.SettingThumbnailIntervalSeconds: "Delay between thumbnail worker passes.",
config.SettingDataDir: "Root data path. Locked because moving storage roots live is risky.", config.SettingDataDir: "Root data path. Locked because moving storage roots live is risky.",
config.SettingActivityRetentionSeconds: "How long activity events stay stored before automatic prune.", config.SettingActivityRetentionSeconds: "How long activity events stay stored before automatic prune.",
config.SettingSecurityEnabled: "Master switch for security middleware, automated bans, suspicious path detection, and upload throttling.",
config.SettingSecurityIPWhitelist: "Comma-separated IPs that bypass generic security bans and rate-limits.", config.SettingSecurityIPWhitelist: "Comma-separated IPs that bypass generic security bans and rate-limits.",
config.SettingSecurityAdminIPWhitelist: "Comma-separated IPs allowed to bypass admin login brute-force controls.", config.SettingSecurityAdminIPWhitelist: "Comma-separated IPs allowed to bypass admin login brute-force controls.",
config.SettingSecurityLoginWindowSecs: "Window used for failed admin login counting.", config.SettingSecurityLoginWindowSecs: "Window used for failed admin login counting.",

View File

@@ -265,6 +265,7 @@ func clearAdminSettingsEnv(t *testing.T) {
"WARPBOX_BOX_POLL_INTERVAL_MS", "WARPBOX_BOX_POLL_INTERVAL_MS",
"WARPBOX_THUMBNAIL_BATCH_SIZE", "WARPBOX_THUMBNAIL_BATCH_SIZE",
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS", "WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
"WARPBOX_SECURITY_ENABLED",
"WARPBOX_SECURITY_IP_WHITELIST", "WARPBOX_SECURITY_IP_WHITELIST",
"WARPBOX_SECURITY_ADMIN_IP_WHITELIST", "WARPBOX_SECURITY_ADMIN_IP_WHITELIST",
"WARPBOX_TRUSTED_PROXY_CIDRS", "WARPBOX_TRUSTED_PROXY_CIDRS",

View File

@@ -51,7 +51,9 @@ func Run(addr string) error {
alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")), alertStore: alerts.NewStore(filepath.Join(cfg.DBDir, "alerts.json")),
securityGuard: security.NewGuard(), securityGuard: security.NewGuard(),
} }
app.reloadSecurityConfig() if err := app.reloadSecurityConfig(); err != nil {
return err
}
router := gin.Default() router := gin.Default()
router.Use(app.securityMiddleware()) router.Use(app.securityMiddleware())

View File

@@ -156,6 +156,9 @@ func (app *App) maxRequestBodyBytes() int64 {
} }
func (app *App) enforceUploadRateLimit(ctx *gin.Context, size int64) bool { func (app *App) enforceUploadRateLimit(ctx *gin.Context, size int64) bool {
if !app.securityFeaturesEnabled() || app.securityGuard == nil {
return true
}
ip := app.clientIP(ctx) ip := app.clientIP(ctx)
if app.securityGuard.IsWhitelisted(ip) || app.securityGuard.IsAdminWhitelisted(ip) { if app.securityGuard.IsWhitelisted(ip) || app.securityGuard.IsAdminWhitelisted(ip) {
return true return true

1
run.sh
View File

@@ -26,6 +26,7 @@ export WARPBOX_BOX_POLL_INTERVAL_MS="${WARPBOX_BOX_POLL_INTERVAL_MS:-5000}"
export WARPBOX_THUMBNAIL_BATCH_SIZE="${WARPBOX_THUMBNAIL_BATCH_SIZE:-10}" export WARPBOX_THUMBNAIL_BATCH_SIZE="${WARPBOX_THUMBNAIL_BATCH_SIZE:-10}"
export WARPBOX_THUMBNAIL_INTERVAL_SECONDS="${WARPBOX_THUMBNAIL_INTERVAL_SECONDS:-30}" export WARPBOX_THUMBNAIL_INTERVAL_SECONDS="${WARPBOX_THUMBNAIL_INTERVAL_SECONDS:-30}"
export WARPBOX_ACTIVITY_RETENTION_SECONDS="${WARPBOX_ACTIVITY_RETENTION_SECONDS:-604800}" export WARPBOX_ACTIVITY_RETENTION_SECONDS="${WARPBOX_ACTIVITY_RETENTION_SECONDS:-604800}"
export WARPBOX_SECURITY_ENABLED="${WARPBOX_SECURITY_ENABLED:-true}"
export WARPBOX_SECURITY_IP_WHITELIST="${WARPBOX_SECURITY_IP_WHITELIST:-}" export WARPBOX_SECURITY_IP_WHITELIST="${WARPBOX_SECURITY_IP_WHITELIST:-}"
export WARPBOX_SECURITY_ADMIN_IP_WHITELIST="${WARPBOX_SECURITY_ADMIN_IP_WHITELIST:-}" export WARPBOX_SECURITY_ADMIN_IP_WHITELIST="${WARPBOX_SECURITY_ADMIN_IP_WHITELIST:-}"
export WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS="${WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS:-600}" export WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS="${WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS:-600}"

View File

@@ -10,6 +10,8 @@
if (!dataNode || !body || !searchInput) return; if (!dataNode || !body || !searchInput) return;
const events = parseData(); const events = parseData();
const initialQuery = new URLSearchParams(window.location.search).get("q");
if (initialQuery) searchInput.value = initialQuery;
function parseData() { function parseData() {
try { try {

View File

@@ -30,6 +30,8 @@
selected: new Set(), selected: new Set(),
activeID: null activeID: null
}; };
const initialQuery = new URLSearchParams(window.location.search).get("q");
if (initialQuery) searchInput.value = initialQuery;
function parseData() { function parseData() {
try { try {