feat/security #2

Merged
kato merged 5 commits from feat/security into master 2026-05-04 00:00:37 +03:00
15 changed files with 78 additions and 12 deletions
Showing only changes of commit 43baade930 - Show all commits

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 {