diff --git a/README.md b/README.md index 1a610ef..0d25613 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ headers = ["Authorization: Bearer token"] **Global settings** (apply to all targets unless overridden): - `refresh_interval`, `timeout`, `follow_redirects`, `accept_redirects`, `receive_alert`, `count` +- `body_size_limit`: Response body cap in bytes (default `1048576` = 1 MiB; `0` means no limit) - `webhook_url`, `webhook_headers`: Default webhook settings - `only`, `skip`: Target filtering arrays - `regions`: AWS regions for remote executors @@ -299,9 +300,12 @@ headers = ["Authorization: Bearer token"] - `method`, `headers`, `body`: HTTP request options - `assert_text`, `should_fail`: Response validation - `skip_ssl`, `follow_redirects`, `accept_redirects`: Connection options +- `body_size_limit`: Per-target response body cap (set to `0` to disable capping for this target) - `webhook_url`, `webhook_headers`: Per-target notifications - `regions`: Target-specific AWS regions +> **Note:** Response bodies are capped at `body_size_limit` bytes when evaluating `assert_text`. If your asserted text appears beyond the cap, the assertion fails and the probe logs a warning (visible in the Recent Logs widget in TUI mode, or on stderr in simple mode). Raise `body_size_limit` or set it to `0` for targets returning large payloads. + ## Multi-Region Monitoring Deploy remote executors as AWS Lambda functions across 13 global regions for distributed monitoring from multiple geographic locations. diff --git a/aws/invocation.go b/aws/invocation.go index 6ca672a..2047384 100644 --- a/aws/invocation.go +++ b/aws/invocation.go @@ -26,6 +26,7 @@ type LambdaRequest struct { SkipSSL bool `json:"skip_ssl"` AssertText string `json:"assert_text"` ShouldFail bool `json:"should_fail"` + BodySizeLimit int64 `json:"body_size_limit,omitempty"` } type LambdaResponse struct { @@ -111,6 +112,7 @@ func invokeLambdaInRegion(url string, config net.NetworkConfig, region string, p SkipSSL: config.SkipSSL, AssertText: config.AssertText, ShouldFail: config.ShouldFail, + BodySizeLimit: config.BodySizeLimit, } payload, err := json.Marshal(request) diff --git a/config/config.go b/config/config.go index da15859..6f291ff 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,7 @@ package config import ( "time" + "github.com/Owloops/updo/net" "github.com/spf13/viper" ) @@ -29,6 +30,7 @@ type Target struct { WebhookURL string `mapstructure:"webhook_url"` WebhookHeaders []string `mapstructure:"webhook_headers"` Regions []string `mapstructure:"regions"` + BodySizeLimit *int64 `mapstructure:"body_size_limit"` } // BoolVal returns the value of a *bool, or the fallback if nil. @@ -39,6 +41,14 @@ func BoolVal(p *bool, fallback bool) bool { return fallback } +// Int64Val returns the value of an *int64, or the fallback if nil. +func Int64Val(p *int64, fallback int64) int64 { + if p != nil { + return *p + } + return fallback +} + type Global struct { RefreshInterval int `mapstructure:"refresh_interval"` Timeout int `mapstructure:"timeout"` @@ -55,6 +65,7 @@ type Global struct { WebhookURL string `mapstructure:"webhook_url"` WebhookHeaders []string `mapstructure:"webhook_headers"` Regions []string `mapstructure:"regions"` + BodySizeLimit int64 `mapstructure:"body_size_limit"` } type Config struct { @@ -72,6 +83,7 @@ func LoadConfig(configFile string) (*Config, error) { viper.SetDefault("global.receive_alert", true) viper.SetDefault("global.count", 0) viper.SetDefault("global.method", _defaultMethod) + viper.SetDefault("global.body_size_limit", net.DefaultBodySizeLimit) if err := viper.ReadInConfig(); err != nil { return nil, err @@ -110,6 +122,10 @@ func LoadConfig(configFile string) (*Config, error) { v := config.Global.SkipSSL target.SkipSSL = &v } + if target.BodySizeLimit == nil { + v := config.Global.BodySizeLimit + target.BodySizeLimit = &v + } if target.WebhookURL == "" && config.Global.WebhookURL != "" { target.WebhookURL = config.Global.WebhookURL } diff --git a/config/config_test.go b/config/config_test.go index f85cb63..6c7692c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -4,6 +4,8 @@ import ( "os" "testing" "time" + + "github.com/Owloops/updo/net" ) func TestLoadConfig(t *testing.T) { @@ -151,6 +153,104 @@ name = "Inherit" } } +// TestBodySizeLimitInheritance verifies that a target inherits +// global.body_size_limit when unset, overrides when explicitly set, +// and supports 0 as "unlimited". +func TestBodySizeLimitInheritance(t *testing.T) { + configContent := ` +[global] +body_size_limit = 2097152 + +[[targets]] +url = "https://inherit.example.com" +name = "Inherit" + +[[targets]] +url = "https://override.example.com" +name = "Override" +body_size_limit = 524288 + +[[targets]] +url = "https://unlimited.example.com" +name = "Unlimited" +body_size_limit = 0 +` + + tmpFile, err := os.CreateTemp("", "test-config-bodysize-*.toml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer func() { + if err := os.Remove(tmpFile.Name()); err != nil { + t.Logf("Failed to remove temp file: %v", err) + } + }() + + if _, err := tmpFile.WriteString(configContent); err != nil { + t.Fatalf("Failed to write config: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + cfg, err := LoadConfig(tmpFile.Name()) + if err != nil { + t.Fatalf("LoadConfig failed: %v", err) + } + + if cfg.Global.BodySizeLimit != 2097152 { + t.Errorf("Global.BodySizeLimit = %d, want 2097152", cfg.Global.BodySizeLimit) + } + + if got := Int64Val(cfg.Targets[0].BodySizeLimit, -1); got != 2097152 { + t.Errorf("Inherit target: BodySizeLimit = %d, want 2097152 (inherited)", got) + } + if got := Int64Val(cfg.Targets[1].BodySizeLimit, -1); got != 524288 { + t.Errorf("Override target: BodySizeLimit = %d, want 524288", got) + } + if got := Int64Val(cfg.Targets[2].BodySizeLimit, -1); got != 0 { + t.Errorf("Unlimited target: BodySizeLimit = %d, want 0 (explicit unlimited)", got) + } +} + +// TestBodySizeLimitDefault verifies that when neither global nor target +// set body_size_limit, the viper default of net.DefaultBodySizeLimit applies. +func TestBodySizeLimitDefault(t *testing.T) { + configContent := ` +[[targets]] +url = "https://example.com" +` + + tmpFile, err := os.CreateTemp("", "test-config-bodysize-default-*.toml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer func() { + if err := os.Remove(tmpFile.Name()); err != nil { + t.Logf("Failed to remove temp file: %v", err) + } + }() + + if _, err := tmpFile.WriteString(configContent); err != nil { + t.Fatalf("Failed to write config: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + cfg, err := LoadConfig(tmpFile.Name()) + if err != nil { + t.Fatalf("LoadConfig failed: %v", err) + } + + if cfg.Global.BodySizeLimit != net.DefaultBodySizeLimit { + t.Errorf("Global.BodySizeLimit = %d, want %d (default)", cfg.Global.BodySizeLimit, net.DefaultBodySizeLimit) + } + if got := Int64Val(cfg.Targets[0].BodySizeLimit, -1); got != net.DefaultBodySizeLimit { + t.Errorf("Target BodySizeLimit = %d, want %d (default inherited)", got, net.DefaultBodySizeLimit) + } +} + func TestLoadConfigDefaults(t *testing.T) { configContent := ` [[targets]] diff --git a/example-config.toml b/example-config.toml index 75bdc90..3248733 100644 --- a/example-config.toml +++ b/example-config.toml @@ -6,6 +6,7 @@ accept_redirects = false receive_alert = false count = 0 skip = ["GitHub-API"] +# body_size_limit = 1048576 # Cap response body reads at 1 MiB (default); 0 = no limit # regions = ["us-east-1", "eu-central-1", "ap-southeast-1"] [[targets]] diff --git a/lambda/lambda.go b/lambda/lambda.go index 2799955..c0f7b8c 100644 --- a/lambda/lambda.go +++ b/lambda/lambda.go @@ -48,6 +48,7 @@ type CheckRequest struct { SkipSSL bool `json:"skip_ssl"` AssertText string `json:"assert_text"` ShouldFail bool `json:"should_fail"` + BodySizeLimit int64 `json:"body_size_limit,omitempty"` } type CheckResponse struct { @@ -110,7 +111,7 @@ func handleRequest(ctx context.Context, req CheckRequest) (CheckResponse, error) Headers: req.Headers, Method: req.Method, Body: req.Body, - BodySizeLimit: net.DefaultBodySizeLimit, + BodySizeLimit: req.BodySizeLimit, } result := net.CheckWebsite(req.URL, netConfig) diff --git a/simple/monitoring.go b/simple/monitoring.go index cd57cfa..4dd69aa 100644 --- a/simple/monitoring.go +++ b/simple/monitoring.go @@ -214,7 +214,7 @@ func monitorTargetSimple(ctx context.Context, target config.Target, targetIndex Headers: target.Headers, Method: target.Method, Body: target.Body, - BodySizeLimit: net.DefaultBodySizeLimit, + BodySizeLimit: config.Int64Val(target.BodySizeLimit, net.DefaultBodySizeLimit), } regions := target.Regions diff --git a/tui/monitoring.go b/tui/monitoring.go index 219dec8..5f152e1 100644 --- a/tui/monitoring.go +++ b/tui/monitoring.go @@ -279,7 +279,7 @@ func monitorTargetTUI(ctx context.Context, target config.Target, targetIndex int Headers: target.Headers, Method: target.Method, Body: target.Body, - BodySizeLimit: net.DefaultBodySizeLimit, + BodySizeLimit: config.Int64Val(target.BodySizeLimit, net.DefaultBodySizeLimit), } regions := target.Regions