* 🐛 keep the stream lease alive when updateIntervalSeconds is 1
handleStreamCache takes a lease so a single node polls LAPI per interval,
and stores it for updateInterval-1 seconds. The e2e stream scenario now
sets updateIntervalSeconds to 1, which makes that a 0 second duration:
golang-ttl-map returns early on a zero ttl (map.go:114) and redis rejects
a non positive EX, so the lease is never stored and the guard silently
does nothing.
Floor the duration at 1 second. At an interval of 1 the lease can survive
a tick that fires slightly early and cost one skipped poll, which is far
better than every node polling every tick against a shared redis.
* Adjust lease duration to prevent cache update conflicts
Updated lease duration logic to ensure a minimum of 1 second.
---------
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* feat: Allow cache reading from replicas
* 🍱 fix logic
* ✨ add testing for redis with mock
* 🍱 fix permission
* 📝 test(e2e/redis): fix swapped IP→verdict comments
The mock returns "f" (not banned) for 1.2.3.4 and "t" (banned) for
1.2.3.5, and the run.sh assertions match that. Both doc comments
described the opposite mapping; correct them to match the code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ✅ test(e2e/redis): exercise read-from-replica path
The redis scenario only set redisCacheHost, so it validated the writer
but never the round-robin reader path this feature adds. Split the mock
into two roles: the primary (--redis-addr) now answers every GET with a
miss, while the replica (--redis-read-addr) serves the hardcoded
verdicts. The scenario points redisCacheReadHosts at the replica (twice,
to drive round-robin), so the banned-IP-blocked assertion only passes if
the plugin actually reads decisions from the replica rather than the
primary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 📝 docs: note replicas don't fall back to primary on outage
When RedisCacheReadHosts is set, reads are not retried against the
primary if the replicas are unreachable. Document that this, combined
with the default RedisCacheUnreachableBlock=true, means a replica outage
can block traffic while the primary is healthy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🐛 fix(cache): avoid nil-pointer panic on empty redis read
redisCache.get fell through to `switch err.Error()` when Get returned a
nil error with an empty value, panicking on the nil error. simpleredis
never returns that combination today (a miss yields RedisMiss), so it
was unreachable in practice — but the read path is safer treating an
empty, error-free read as a cache miss, which also guarantees err is
non-nil before err.Error() is called.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🍱 add test for rotation
* 🐛 readd redis/run.sh
* 🐛 fix redis/run.sh
* 🐛 fix test label; remove log
* 🍱 add tests for roundRobin
* 🐛 fix test
---------
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
Co-authored-by: mhx <mathieu@hanotaux.fr>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Do not consider body unreadable when it's http.NoBody
Fix#351
* 🐛 appsec: only drop unreadable bodies for methods that carry one
Gate the crowdsecAppsecUnreadableBodyBlock drop on the request method,
mirroring the reference lua-cs-bouncer METHODS_WITH_BODY list
(POST/PUT/PATCH/DELETE). Browser GETs over HTTP/3 never carry a
Content-Length (quic-go always wraps the stream in a non-nil body with
ContentLength -1), so they were all wrongly dropped; they are now
forwarded to appsec headers-only, while gRPC streams (always POST) stay
droppable.
Fix#351
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🍱 add test and remove useless comment
* 🍱 add test for GET with unreadable body
* 🍱 fix log
* 🍱 fix test
---------
Co-authored-by: mhx <mathieu@hanotaux.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* 🐛 fix appsec silently 403-ing gRPC streams with unreadable body
A bidirectional gRPC stream is an HTTP/2 request with no Content-Length
whose body never reaches EOF. Since #321 removed the ContentLength guard,
appsecQuery buffered it with io.ReadAll, which blocked until the request
timed out and was turned into a 403 (issue #323). The backend was never
reached (OriginStatus:0).
Mirror the reference lua-cs-bouncer behaviour: detect an unreadable body
(ProtoMajor >= 2 && ContentLength < 0) and, instead of buffering it,
forward the request to Appsec with headers only. Add a new
CrowdsecAppsecDropUnreadableBody option (default false) that mirrors the
reference APPSEC_DROP_UNREADABLE_BODY: when true, such requests are
blocked outright instead of forwarded without their body.
Readable HTTP/1.1 bodies are still buffered and inspected, so the bypass
closed by #321 stays closed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🚨 appsec: satisfy linters (gocritic ifElseChain, misspell)
Rewrite the body-handling if/else chain in appsecQuery as a switch
(gocritic) and use US spelling "behavior" (misspell).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🔇 appsec: drop redundant unreadable-body debug log
Address review on #332: the caller (handleNextServeHTTP) already logs the
returned error with the request IP, so the inner Debug line duplicated it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🍱 increase gocyclo
* 🍱 fix lint
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* Consider 502, 503 and 504 as unavaible for appsec
Fixes#337
* ✨ add test and the function isReverseProxyError
---------
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* Add parameter to configure Ban Response Content-Type
* Add testing for new BanResponseContentType parameter
* Ensure there is a fallback to default Content-Type is user provided empty value
* Set Content-Type even if banTemplate is nil
* Add more edge cases for testing ban response Content-Type
* Add CR/LF validation for BanResponseContentType
* Add CaptchaResponseContentType to allow separate Content-Type configuration for captcha responses
* Add testing for new CaptchaResponseContentType
* Update README
* Split nil and CR/LF response Content-Type value validation into separate function
* Throw error instead of setting the default in case of empty parameter declaration
* Update testing accordingly
* ✨ remove HTML from var name, add tests and infer content type from filePath
* 🍱 fix lint ?
* 🍱 fix lint
* 🍱 fix lint
* 🍱 fix lint + naming
* 🍱 fix lint
* 🍱 fuck lint
---------
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* 🍱 not block on stream startup
* 🍱 fix
* 🍱 fix lint
* 🍱 fix lint
* 🍱 fix lint
* 🍱 update readme
* 📚 clarify StreamStartupBlock warning in README
Make the fail-open implication of StreamStartupBlock=false explicit:
banned IPs are allowed through until the first stream sync completes.
* 🐛 clear isCrowdsecStreamStartup on alreadyUpdated path
When cacheTimeoutKey is already set (another instance/process has
populated the stream cache within the update window), the early return
in handleStreamCache used to leave isCrowdsecStreamStartup at true,
causing this instance to keep sending startup=true on subsequent ticks.
Flip the flag in the early-return path so startup is correctly tracked
across multi-instance deployments sharing a cache.
---------
Co-authored-by: mhx <mathieu@hanotaux.fr>
* cicd(versioning): Automate proper version tagging in the bouncer.go that is reported to Crowdsec LAPI
* cicd(versioning): Move pluginVersion to it's own file
* Disablie lint check for no global variables
* Remove trailing new-line
* Update pluginVersion declaration style
* 🍱 Simplify for tests
Removed the verification step after updating the version and modified the commit message to include an emoji.
---------
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* 🐛 fix start up config error for appsec
* :doc: add documentation on appsec variables and missing conf parameter
* 🍱 fix lint
* 🍱 fix lint
* 🍱 fix lint
* 🍱 fix after lot of tests
* update exemple tls with new variables tested
* fix exemple appsec with release and not localplugin
---------
Co-authored-by: mhx <mathieu@hanotaux.fr>
* Add parameter to specify Request header value to inject in HTML template
* Add documentation for specifying request header value
* Fix example in docs for CustomHeader
* Update parameter name to traceCustomHeader
* Ensure naming is consitent with project
* xx
* Remove outdated comment on decision caching
Remove comment about decision caching based on timeout.
---------
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI
* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI
* 🐛 fix(user-agent) remove whitespaces
* 🐛 fix(user-agent) Add Cap for Bouncer
---------
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* ✨ Add ban html template
* 📝 Add doc for custom ban page
* ⚰️ Remove old code due to merge
* ✏️ Fix merge remaining in html
* ✨ render banTemplate from html/template to string
* 🚨 : fix lint
---------
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>