diff --git a/.agents/skills/echo-guard/SKILL.md b/.agents/skills/echo-guard/SKILL.md index 98200d3..3fc3b8c 100644 --- a/.agents/skills/echo-guard/SKILL.md +++ b/.agents/skills/echo-guard/SKILL.md @@ -1,11 +1,11 @@ --- name: echo-guard -description: Use when wiring guard-core-go security into a Go Echo service, or when working in github.com/rennf93/echo-guard: build the middleware with guardecho.New(engine, opts...) (package echo, alias it to avoid clashing with github.com/labstack/echo/v4), tune WithMaxBodyBytes (default 262144) and WithLogger, attach route identity with WithRouteID on the request context (c.SetRequest), understand requestShim adaptation of echo.Context to guardcore.Request including the bounded body prefix scan, idempotent Body(), and replay so handlers still receive the full stream, exact verdict translation into c.Response() writes with the chain stopped by not calling next(c) (Echo has no Abort call), fail-closed 500 on engine malfunction, and Redis-backed integration tests via REDIS_HOST and go test -tags integration. +description: Use when wiring guard-core-go security into a Go Echo service, or when working in github.com/rennf93/echo-guard: build the middleware with guardecho.New(engine, opts...) (package echo, alias it to avoid clashing with github.com/labstack/echo/v5), tune WithMaxBodyBytes (default 262144) and WithLogger, attach route identity with WithRouteID on the request context (c.SetRequest), understand requestShim adaptation of echo.Context to guardcore.Request including the bounded body prefix scan, idempotent Body(), and replay so handlers still receive the full stream, exact verdict translation into c.Response() writes with the chain stopped by not calling next(c) (Echo has no Abort call), fail-closed 500 on engine malfunction, and Redis-backed integration tests via REDIS_HOST and go test -tags integration. --- # echo-guard -Echo middleware adapter for [guard-core-go](https://github.com/Guard-Core/guard-core-go). Translates `echo.Context` into the guardcore request surface, runs the engine, and translates verdicts to exact Echo responses (status, headers, body, then stop the chain). Contains no security logic itself. Module: `github.com/rennf93/echo-guard`, Go `1.25.0`, released at `v1.1.0`. +Echo middleware adapter for [guard-core-go](https://github.com/Guard-Core/guard-core-go). Translates `echo.Context` into the guardcore request surface, runs the engine, and translates verdicts to exact Echo responses (status, headers, body, then stop the chain). Contains no security logic itself. Module: `github.com/rennf93/echo-guard/v2`, Go `1.26.0`, Echo v5 (v2.0.0; Echo v4 users stay on the v1.x line). ## Quick Reference @@ -22,16 +22,16 @@ Behavior contract: the engine sees at most `MaxBodyBytes` of the body prefix; pa ## Installation ```sh -go get github.com/rennf93/echo-guard@v1.1.0 github.com/rennf93/guard-core-go/v4@v4.0.5-0.20260926230539-e39ac203568b +go get github.com/rennf93/echo-guard/v2@v2.0.0 github.com/rennf93/guard-core-go/v4@v4.4.1 ``` -The package name is `echo`, which collides with `github.com/labstack/echo/v4`, so import the adapter with an alias: +The package name is `echo`, which collides with `github.com/labstack/echo/v5`, so import the adapter with an alias: ```go import ( - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" - guardecho "github.com/rennf93/echo-guard" + guardecho "github.com/rennf93/echo-guard/v2" ) ``` @@ -48,7 +48,7 @@ if err != nil { log.Fatal(err) } router := echolib.New() router.Use(guard) -router.GET("/", func(c echolib.Context) error { return c.String(200, "ok") }) +router.GET("/", func(c *echolib.Context) error { return c.String(200, "ok") }) log.Fatal(router.Start(":8080")) ``` @@ -99,7 +99,7 @@ func WithRouteID(ctx context.Context, routeID string) context.Context ## Footguns -- Package name collision: `github.com/rennf93/echo-guard` and `github.com/labstack/echo/v4` are both package `echo`; always alias the adapter (for example `guardecho`). +- Package name collision: `github.com/rennf93/echo-guard/v2` and `github.com/labstack/echo/v5` are both package `echo`; always alias the adapter (for example `guardecho`). - The engine only scans the first `MaxBodyBytes` bytes. A malicious marker beyond the bound is not detected and the request passes; verify bounds when a route accepts large payloads. - `WithMaxBodyBytes(-1)` and `WithLogger(nil)` are silently ignored, not errors. Guard against typos that drop real configuration. - Client identity comes from `RemoteAddr`, never `c.RealIP()`. Do not "fix" this in the adapter; proxy trust is core configuration territory. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9dca3c..35a6b04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ name: Release Gate # can only run once the tag exists. # # Re-check: run this workflow by hand (Actions > Release Gate > Run workflow) -# with the tag, for example v1.5.0. It checks out that tag, so a fixed +# with the tag, for example v2.0.0. It checks out that tag, so a fixed # workflow on the default branch can re-check an existing tag. on: push: @@ -21,7 +21,7 @@ on: workflow_dispatch: inputs: tag: - description: 'Release tag to check, for example v1.5.0' + description: 'Release tag to check, for example v2.0.0' required: true permissions: {} @@ -58,7 +58,7 @@ jobs: tag="$REF_NAME" fi if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "::error::tag '$tag' is not of the form v1.2.3" + echo "::error::tag '$tag' is not of the form v2.0.0" exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" @@ -119,7 +119,7 @@ jobs: - name: Verify module tag resolves env: - MODULE: github.com/rennf93/echo-guard + MODULE: github.com/rennf93/echo-guard/v2 TAG: ${{ needs.verify.outputs.tag }} run: | cd "$(mktemp -d)" diff --git a/AGENTS.md b/AGENTS.md index ff942b2..df0130c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,9 +5,10 @@ Guidance for AI agents (including Claude Code) working in this repository. echo-guard is an Echo middleware adapter for [guard-core-go](https://github.com/Guard-Core/guard-core-go). It translates `echo.Context` into the guardcore request surface, runs the engine, and translates verdicts to exact Echo responses. It contains NO security logic of its own. -- Module: `github.com/rennf93/echo-guard`, Go directive `go 1.25.0`, MIT license. +- Module: `github.com/rennf93/echo-guard/v2`, Go directive `go 1.26.0` (echo v5.4.0 itself needs 1.25.0), MIT license. - Single Go package `echo` at the repo root. Source files: `middleware.go`, `request.go`. Tests: `middleware_test.go`, `integration_test.go`. There are no subpackage directories. -- Release state: no tags exist yet. The repo ships untagged; depend on it via a commit reference until the first tag is cut. State this honestly; there is no changelog beyond git history. +- Release state: v1.x targets Echo v4 (last release v1.5.0). This branch is the Echo v5 line, module `github.com/rennf93/echo-guard/v2`, released as v2.0.0 (see `CHANGELOG.md`). Echo v4 users stay on v1.x. +- Echo v5 notes: `echo.Context` is a concrete struct, so handlers and middleware take `*echo.Context`; `c.Response()` returns an `http.ResponseWriter` (the `*echo.Response` is reached with `echo.UnwrapResponse`); `c.Error` is gone (the error handler is `c.Echo().HTTPErrorHandler`). - The public surface is small and deliberate: `New`, `Option`, `WithMaxBodyBytes`, `WithLogger`, `WithRouteID`, and the `DefaultMaxBodyBytes` constant (262144). ## Ecosystem Position @@ -15,9 +16,9 @@ echo-guard is an Echo middleware adapter for [guard-core-go](https://github.com/ This repo is the ADAPTER layer of the guard-core ecosystem: - `guard-core-go` is the engine. All detection (suspicious activity, IP bans, rate limits, HTTPS enforcement), verdict construction, and error response factories live there. -- This repo wires Go Echo types to that engine and nothing more. It consumes `github.com/rennf93/guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b` as a normal module dependency (see `go.mod`); no `replace` directive is used or needed. For cross-repo work on the core, add a temporary local `replace` in your own checkout and drop it before committing. +- This repo wires Go Echo types to that engine and nothing more. It consumes `github.com/rennf93/guard-core-go/v4 v4.4.1` as a normal module dependency (see `go.mod`); no `replace` directive is used or needed. For cross-repo work on the core, add a temporary local `replace` in your own checkout and drop it before committing. - Because the middleware is an `echo.MiddlewareFunc`, it composes with `e.Use` and runs anywhere in an Echo handler chain. Echo has no explicit abort call: a verdict is written to `c.Response()` and the chain stops because the middleware returns nil without calling `next(c)`. -- Direct dependencies: `github.com/labstack/echo/v4 v4.15.4` (the framework this adapter exists for; a framework import is allowed here, unlike in the core) and `github.com/rennf93/guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b`. Notable indirect floors: the core's transitives (`redis/go-redis/v9 v9.22.0`, `dlclark/regexp2 v1.12.0`, `go.uber.org/atomic`, `golang.org/x/text v0.41.0`) plus echo's own (`golang.org/x/crypto v0.53.0`, `golang.org/x/net v0.56.0`). govulncheck reports zero vulnerabilities reachable from this module's code; the module-level findings in `x/crypto/ssh` and `x/crypto/openpgp` are never called here, so no floor bump was needed (unlike gin-guard's quic-go bump for GO-2026-5676). +- Direct dependencies: `github.com/labstack/echo/v5 v5.4.0` (the framework this adapter exists for; a framework import is allowed here, unlike in the core) and `github.com/rennf93/guard-core-go/v4 v4.4.1`. Notable indirect floors: the core's transitives (`redis/go-redis/v9 v9.22.0`, `dlclark/regexp2 v1.12.0`, `go.uber.org/atomic`, `golang.org/x/text v0.41.0`) plus echo's own (`golang.org/x/crypto v0.53.0`, `golang.org/x/net v0.56.0`). govulncheck reports zero vulnerabilities reachable from this module's code; the module-level findings in `x/crypto/ssh` and `x/crypto/openpgp` are never called here, so no floor bump was needed (unlike gin-guard's quic-go bump for GO-2026-5676). ## Boundary Rules @@ -48,9 +49,9 @@ Minimal usage (from README.md): ```go import ( - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" - guardecho "github.com/rennf93/echo-guard" + guardecho "github.com/rennf93/echo-guard/v2" ) cfg := guardcore.DefaultSecurityConfig() @@ -63,15 +64,15 @@ if err != nil { log.Fatal(err) } router := echolib.New() router.Use(guard) -router.GET("/api", func(c echolib.Context) error { return c.String(200, "ok") }) +router.GET("/api", func(c *echolib.Context) error { return c.String(200, "ok") }) log.Fatal(router.Start(":8080")) ``` -Note the import alias: the module path ends in `echo-guard` but the package name is `echo`, which collides with `github.com/labstack/echo/v4` (also package `echo`), so an explicit alias such as `guardecho "github.com/rennf93/echo-guard"` is the idiomatic import. +Note the import alias: the module path ends in `echo-guard` but the package name is `echo`, which collides with `github.com/labstack/echo/v5` (also package `echo`), so an explicit alias such as `guardecho "github.com/rennf93/echo-guard/v2"` is the idiomatic import. ## Development Commands -There is no Makefile. Every command below comes from the CI workflows or the README. +A Makefile exists (`install`, `test`, `lint`, `bump-version`, `clean`, Docker based), but the commands below come from the CI workflows (the build files win over this doc). | Command | Purpose | Verified in | | --- | --- | --- | @@ -85,7 +86,7 @@ There is no Makefile. Every command below comes from the CI workflows or the REA | `docker compose -f examples/simple_app/docker-compose.yml up --build -d --wait` | Local live smoke of the guarded example app (assertions in `.github/workflows/live-smoke.yml`) | `.github/workflows/live-smoke.yml` | | `mkdocs build --strict` | Build the MkDocs site (docs.yml does the same before gh-deploy) | `.github/workflows/docs.yml` | -CI runs the test job on a Go matrix of `1.25.x` and `1.26.x` (fail-fast disabled) with `GOTOOLCHAIN: auto`, plus a separate `govulncheck` job on stable Go. The integration step runs against a `redis:7-alpine` service container on port 6379 with `REDIS_HOST=127.0.0.1`. +CI runs the test job on Go `1.26.x` (a one-entry matrix, fail-fast disabled) with `GOTOOLCHAIN: auto`, plus a separate `govulncheck` job on stable Go. The integration step runs against a `redis:7-alpine` service container on port 6379 with `REDIS_HOST=127.0.0.1`. ## Project Structure @@ -95,7 +96,7 @@ CI runs the test job on a Go matrix of `1.25.x` and `1.26.x` (fail-fast disabled ├── request.go # requestShim (implements guardcore.Request), WithRouteID, DefaultMaxBodyBytes, replayBody ├── middleware_test.go # unit tests, real echo.Echo, Redis disabled ├── integration_test.go # //go:build integration, Redis-backed, skips when REDIS_HOST is unset -├── go.mod / go.sum # module github.com/rennf93/echo-guard, requires echo v4.15.4 and guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b +├── go.mod / go.sum # module github.com/rennf93/echo-guard/v2, requires echo v5.4.0 and guard-core-go/v4 v4.4.1 ├── README.md # usage, options, integration test instructions ├── LICENSE # MIT ├── mkdocs.yml # MkDocs site config (material theme; nav: Home/Usage/Configuration) @@ -125,16 +126,16 @@ CI runs the test job on a Go matrix of `1.25.x` and `1.26.x` (fail-fast disabled ## Technology Stack -- Go, directive `go 1.25.0`; CI matrix tests 1.25.x and 1.26.x. -- `github.com/rennf93/guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b` (direct require in `go.mod`), providing `guardcore.Engine`, `guardcore.Request`, `guardcore.Response`, `guardcore.SecurityConfig`. -- `github.com/labstack/echo/v4 v4.15.4` (direct require in `go.mod`), providing `echo.MiddlewareFunc` and `echo.Context` for the bridging surface. +- Go, directive `go 1.26.0`; CI runs the test job on 1.26.x. +- `github.com/rennf93/guard-core-go/v4 v4.4.1` (direct require in `go.mod`), providing `guardcore.Engine`, `guardcore.Request`, `guardcore.Response`, `guardcore.SecurityConfig`. +- `github.com/labstack/echo/v5 v5.4.0` (direct require in `go.mod`), providing `echo.MiddlewareFunc` and `echo.Context` for the bridging surface. - Redis 7 for integration tests (CI service container `redis:7-alpine`); runtime Redis usage is a guard-core-go concern, not this adapter's. - GitHub Actions: CI on push and pull_request, Release Gate on `v*` tag push, weekly Scheduled Lint, CodeQL (go), dockerized Live Smoke of the example apps on push/PR to master, MkDocs docs deploy to GitHub Pages on docs pushes to master, demo container publishing to ghcr.io on releases and examples changes, daily upstream drift tests against guard-core-go@master, issue-link, greetings, labeler, stale, summary, and label sync; Dependabot for gomod and actions. All workflows use minimal permissions and pinned action SHAs. ## Testing Guidelines - Unit tests live in `middleware_test.go` and run with plain `go test ./...`. The helper `newTestEngine` builds a config with `EnableRedis = false` so no Redis is required. -- Tests drive a real `echo.Echo` (see the `serve` helper): the guard is registered with `router.Use`, a wildcard `router.Any("/*")` probe route records whether it was reached, the response headers it saw, and the method, path, query, and body it received. `newShimContext` builds a bare `echolib.Context` via `e.NewContext(r, httptest.NewRecorder())` for direct `requestShim` tests. +- Tests drive a real `echo.Echo` (see the `serve` helper): the guard is registered with `router.Use`, a wildcard `router.Any("/*")` probe route records whether it was reached, the response headers it saw, and the method, path, query, and body it received. `newShimContext` builds a bare `*echolib.Context` via `e.NewContext(r, httptest.NewRecorder())` for direct `requestShim` tests. - Integration tests in `integration_test.go` carry the `//go:build integration` build tag. They skip (not fail) when `REDIS_HOST` is unset. They use `cfg.RedisURL = "redis://" + host + ":6379"`, `cfg.RedisPrefix = "guard_core_echo_test:"`, `cfg.RedisFailOpen = false`, and clean up the `banned_ips:*` and `rate_limit:rate:*` key patterns in `t.Cleanup` before closing the engine. - Invariants the tests enforce; keep them true when changing code: - A blocked request must never reach the handler; a clean pass must reach it with the original method, path, and query, and no adapter-added headers. diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a6cc1..18626b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ Release Notes ___ +v2.0.0 (2026-10-19) +------------------- + +Echo v5 support as the `/v2` module +----------------------------------- + +### Changed + +- **Echo v5 support; module path `github.com/rennf93/echo-guard/v2`; Echo v4 users stay on v1.x.** The adapter now targets `github.com/labstack/echo/v5 v5.4.0`. Go's semantic import versioning requires the `/v2` suffix because the public types change (`echo.MiddlewareFunc`, `echo.HandlerFunc` and `echo.Context` are different types in v5). Import the adapter as `github.com/rennf93/echo-guard/v2` (keep an alias such as `guardecho`, the package name is still `echo`). Echo v4 applications keep using `github.com/rennf93/echo-guard` v1.x, which stays on `echo/v4`. The adapter behavior is identical: exact verdict translation, fail-closed 500, bounded body replay, route identity from the request context, `RemoteAddr` as the only client host. +- **Raised the engine floor to `github.com/rennf93/guard-core-go/v4 v4.4.1`.** v4.4.1 stops printing the spec DEBUG lines (`No countries blocked or whitelisted `, `Request blocked by `, the private-peer spoof line) on every request; they go through `log/slog` at debug level. +- **Forced signature changes** (every other exported identifier is unchanged): + - `New(engine, opts...)` returns `echo.MiddlewareFunc` of Echo v5 (`func(next HandlerFunc) HandlerFunc` over `func(c *echo.Context) error`). + - `GuardWebSocket(engine, c)` takes `*echo.Context` instead of the v4 `echo.Context` interface. + - `NewStatusHandler(engine)` returns Echo v5's `echo.HandlerFunc` (`func(c *echo.Context) error`). + - `AddStatusRoute(app, engine, path)` takes Echo v5's `*echo.Echo`. +- **Behavior notes for the v5 port.** Echo v5 removed `c.Error` and exposes the response as an `http.ResponseWriter`: a handler error is turned into a response through `c.Echo().HTTPErrorHandler`, and the adapter reaches the `*echo.Response` with `echo.UnwrapResponse` to swap the capture and held writers underneath it, as v4 did through `Response.Writer`. A context whose response writer chain never reaches an `*echo.Response`, or a handler error on a context without an Echo instance, fails closed with the 500 `Security check failed` response. + +___ + v1.5.0 (2026-10-16) ------------------- diff --git a/CLAUDE.md b/CLAUDE.md index 7536189..c30f984 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,9 +5,10 @@ Guidance for AI agents (including Claude Code) working in this repository. echo-guard is an Echo middleware adapter for [guard-core-go](https://github.com/Guard-Core/guard-core-go). It translates `echo.Context` into the guardcore request surface, runs the engine, and translates verdicts to exact Echo responses. It contains NO security logic of its own. -- Module: `github.com/rennf93/echo-guard`, Go directive `go 1.25.0`, MIT license. +- Module: `github.com/rennf93/echo-guard/v2`, Go directive `go 1.26.0` (echo v5.4.0 itself needs 1.25.0), MIT license. - Single Go package `echo` at the repo root. Source files: `middleware.go`, `request.go`. Tests: `middleware_test.go`, `integration_test.go`. There are no subpackage directories. -- Release state: no tags exist yet. The repo ships untagged; depend on it via a commit reference until the first tag is cut. State this honestly; there is no changelog beyond git history. +- Release state: v1.x targets Echo v4 (last release v1.5.0). This branch is the Echo v5 line, module `github.com/rennf93/echo-guard/v2`, released as v2.0.0 (see `CHANGELOG.md`). Echo v4 users stay on v1.x. +- Echo v5 notes: `echo.Context` is a concrete struct, so handlers and middleware take `*echo.Context`; `c.Response()` returns an `http.ResponseWriter` (the `*echo.Response` is reached with `echo.UnwrapResponse`); `c.Error` is gone (the error handler is `c.Echo().HTTPErrorHandler`). - The public surface is small and deliberate: `New`, `Option`, `WithMaxBodyBytes`, `WithLogger`, `WithRouteID`, and the `DefaultMaxBodyBytes` constant (262144). ## Ecosystem Position @@ -15,9 +16,9 @@ echo-guard is an Echo middleware adapter for [guard-core-go](https://github.com/ This repo is the ADAPTER layer of the guard-core ecosystem: - `guard-core-go` is the engine. All detection (suspicious activity, IP bans, rate limits, HTTPS enforcement), verdict construction, and error response factories live there. -- This repo wires Go Echo types to that engine and nothing more. It consumes `github.com/rennf93/guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b` as a normal module dependency (see `go.mod`); no `replace` directive is used or needed. For cross-repo work on the core, add a temporary local `replace` in your own checkout and drop it before committing. +- This repo wires Go Echo types to that engine and nothing more. It consumes `github.com/rennf93/guard-core-go/v4 v4.4.1` as a normal module dependency (see `go.mod`); no `replace` directive is used or needed. For cross-repo work on the core, add a temporary local `replace` in your own checkout and drop it before committing. - Because the middleware is an `echo.MiddlewareFunc`, it composes with `e.Use` and runs anywhere in an Echo handler chain. Echo has no explicit abort call: a verdict is written to `c.Response()` and the chain stops because the middleware returns nil without calling `next(c)`. -- Direct dependencies: `github.com/labstack/echo/v4 v4.15.4` (the framework this adapter exists for; a framework import is allowed here, unlike in the core) and `github.com/rennf93/guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b`. Notable indirect floors: the core's transitives (`redis/go-redis/v9 v9.22.0`, `dlclark/regexp2 v1.12.0`, `go.uber.org/atomic`, `golang.org/x/text v0.41.0`) plus echo's own (`golang.org/x/crypto v0.53.0`, `golang.org/x/net v0.56.0`). govulncheck reports zero vulnerabilities reachable from this module's code; the module-level findings in `x/crypto/ssh` and `x/crypto/openpgp` are never called here, so no floor bump was needed (unlike gin-guard's quic-go bump for GO-2026-5676). +- Direct dependencies: `github.com/labstack/echo/v5 v5.4.0` (the framework this adapter exists for; a framework import is allowed here, unlike in the core) and `github.com/rennf93/guard-core-go/v4 v4.4.1`. Notable indirect floors: the core's transitives (`redis/go-redis/v9 v9.22.0`, `dlclark/regexp2 v1.12.0`, `go.uber.org/atomic`, `golang.org/x/text v0.41.0`) plus echo's own (`golang.org/x/crypto v0.53.0`, `golang.org/x/net v0.56.0`). govulncheck reports zero vulnerabilities reachable from this module's code; the module-level findings in `x/crypto/ssh` and `x/crypto/openpgp` are never called here, so no floor bump was needed (unlike gin-guard's quic-go bump for GO-2026-5676). ## Boundary Rules @@ -48,9 +49,9 @@ Minimal usage (from README.md): ```go import ( - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" - guardecho "github.com/rennf93/echo-guard" + guardecho "github.com/rennf93/echo-guard/v2" ) cfg := guardcore.DefaultSecurityConfig() @@ -63,15 +64,15 @@ if err != nil { log.Fatal(err) } router := echolib.New() router.Use(guard) -router.GET("/api", func(c echolib.Context) error { return c.String(200, "ok") }) +router.GET("/api", func(c *echolib.Context) error { return c.String(200, "ok") }) log.Fatal(router.Start(":8080")) ``` -Note the import alias: the module path ends in `echo-guard` but the package name is `echo`, which collides with `github.com/labstack/echo/v4` (also package `echo`), so an explicit alias such as `guardecho "github.com/rennf93/echo-guard"` is the idiomatic import. +Note the import alias: the module path ends in `echo-guard` but the package name is `echo`, which collides with `github.com/labstack/echo/v5` (also package `echo`), so an explicit alias such as `guardecho "github.com/rennf93/echo-guard/v2"` is the idiomatic import. ## Development Commands -There is no Makefile. Every command below comes from the CI workflows or the README. +A Makefile exists (`install`, `test`, `lint`, `bump-version`, `clean`, Docker based), but the commands below come from the CI workflows (the build files win over this doc). | Command | Purpose | Verified in | | --- | --- | --- | @@ -83,7 +84,7 @@ There is no Makefile. Every command below comes from the CI workflows or the REA | `REDIS_HOST=127.0.0.1 go test -tags integration ./...` | Unit plus Redis-backed integration tests | `.github/workflows/ci.yml` step "go test (integration, redis)", README.md | | `go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...` | Vulnerability scan | `.github/workflows/ci.yml` and `scheduled-lint.yml` | -CI runs the test job on a Go matrix of `1.25.x` and `1.26.x` (fail-fast disabled) with `GOTOOLCHAIN: auto`, plus a separate `govulncheck` job on stable Go. The integration step runs against a `redis:7-alpine` service container on port 6379 with `REDIS_HOST=127.0.0.1`. +CI runs the test job on Go `1.26.x` (a one-entry matrix, fail-fast disabled) with `GOTOOLCHAIN: auto`, plus a separate `govulncheck` job on stable Go. The integration step runs against a `redis:7-alpine` service container on port 6379 with `REDIS_HOST=127.0.0.1`. ## Project Structure @@ -93,7 +94,7 @@ CI runs the test job on a Go matrix of `1.25.x` and `1.26.x` (fail-fast disabled ├── request.go # requestShim (implements guardcore.Request), WithRouteID, DefaultMaxBodyBytes, replayBody ├── middleware_test.go # unit tests, real echo.Echo, Redis disabled ├── integration_test.go # //go:build integration, Redis-backed, skips when REDIS_HOST is unset -├── go.mod / go.sum # module github.com/rennf93/echo-guard, requires echo v4.15.4 and guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b +├── go.mod / go.sum # module github.com/rennf93/echo-guard/v2, requires echo v5.4.0 and guard-core-go/v4 v4.4.1 ├── README.md # usage, options, integration test instructions ├── LICENSE # MIT └── .github/ @@ -106,16 +107,16 @@ CI runs the test job on a Go matrix of `1.25.x` and `1.26.x` (fail-fast disabled ## Technology Stack -- Go, directive `go 1.25.0`; CI matrix tests 1.25.x and 1.26.x. -- `github.com/rennf93/guard-core-go/v4 v4.0.5-0.20260926230539-e39ac203568b` (direct require in `go.mod`), providing `guardcore.Engine`, `guardcore.Request`, `guardcore.Response`, `guardcore.SecurityConfig`. -- `github.com/labstack/echo/v4 v4.15.4` (direct require in `go.mod`), providing `echo.MiddlewareFunc` and `echo.Context` for the bridging surface. +- Go, directive `go 1.26.0`; CI runs the test job on 1.26.x. +- `github.com/rennf93/guard-core-go/v4 v4.4.1` (direct require in `go.mod`), providing `guardcore.Engine`, `guardcore.Request`, `guardcore.Response`, `guardcore.SecurityConfig`. +- `github.com/labstack/echo/v5 v5.4.0` (direct require in `go.mod`), providing `echo.MiddlewareFunc` and `echo.Context` for the bridging surface. - Redis 7 for integration tests (CI service container `redis:7-alpine`); runtime Redis usage is a guard-core-go concern, not this adapter's. - GitHub Actions: CI on push and pull_request, Release Gate on `v*` tag push, weekly Scheduled Lint, CodeQL (go), Dependabot for gomod and actions, all with minimal permissions and pinned action SHAs. ## Testing Guidelines - Unit tests live in `middleware_test.go` and run with plain `go test ./...`. The helper `newTestEngine` builds a config with `EnableRedis = false` so no Redis is required. -- Tests drive a real `echo.Echo` (see the `serve` helper): the guard is registered with `router.Use`, a wildcard `router.Any("/*")` probe route records whether it was reached, the response headers it saw, and the method, path, query, and body it received. `newShimContext` builds a bare `echolib.Context` via `e.NewContext(r, httptest.NewRecorder())` for direct `requestShim` tests. +- Tests drive a real `echo.Echo` (see the `serve` helper): the guard is registered with `router.Use`, a wildcard `router.Any("/*")` probe route records whether it was reached, the response headers it saw, and the method, path, query, and body it received. `newShimContext` builds a bare `*echolib.Context` via `e.NewContext(r, httptest.NewRecorder())` for direct `requestShim` tests. - Integration tests in `integration_test.go` carry the `//go:build integration` build tag. They skip (not fail) when `REDIS_HOST` is unset. They use `cfg.RedisURL = "redis://" + host + ":6379"`, `cfg.RedisPrefix = "guard_core_echo_test:"`, `cfg.RedisFailOpen = false`, and clean up the `banned_ips:*` and `rate_limit:rate:*` key patterns in `t.Cleanup` before closing the engine. - Invariants the tests enforce; keep them true when changing code: - A blocked request must never reach the handler; a clean pass must reach it with the original method, path, and query, and no adapter-added headers. diff --git a/README.md b/README.md index 9d8e7e2..4451bd1 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Go modules published via GitHub releases. **Production-ready.** | [guard-core-go](https://github.com/Guard-Core/guard-core-go) | Go engine | [![release](https://img.shields.io/github/v/tag/Guard-Core/guard-core-go?label=tag)](https://github.com/Guard-Core/guard-core-go/releases) | | [nethttp-guard](https://github.com/Guard-Core/nethttp-guard) | net/http adapter | [![release](https://img.shields.io/github/v/tag/Guard-Core/nethttp-guard?label=tag)](https://github.com/Guard-Core/nethttp-guard/releases) | | [gin-guard](https://github.com/Guard-Core/gin-guard) | Gin adapter | [![release](https://img.shields.io/github/v/tag/Guard-Core/gin-guard?label=tag)](https://github.com/Guard-Core/gin-guard/releases) | -| [echo-guard](https://github.com/Guard-Core/echo-guard) | Echo (v4) adapter | [![release](https://img.shields.io/github/v/tag/Guard-Core/echo-guard?label=tag)](https://github.com/Guard-Core/echo-guard/releases) | +| [echo-guard](https://github.com/Guard-Core/echo-guard) | Echo (v5) adapter | [![release](https://img.shields.io/github/v/tag/Guard-Core/echo-guard?label=tag)](https://github.com/Guard-Core/echo-guard/releases) | | [fiber-guard](https://github.com/Guard-Core/fiber-guard) | Fiber (v3) adapter | [![release](https://img.shields.io/github/v/tag/Guard-Core/fiber-guard?label=tag)](https://github.com/Guard-Core/fiber-guard/releases) | | [guard-agent-go](https://github.com/Guard-Core/guard-agent-go) | Telemetry agent | [![release](https://img.shields.io/github/v/tag/Guard-Core/guard-agent-go?label=tag)](https://github.com/Guard-Core/guard-agent-go/releases) | @@ -134,7 +134,7 @@ ___ ## Features -- **The full guard-core-go engine pipeline** over Echo (v4) requests: rate limiting, IP policy, payload inspection across 19 attack categories, auto-banning +- **The full guard-core-go engine pipeline** over Echo (v5) requests: rate limiting, IP policy, payload inspection across 19 attack categories, auto-banning - **Security headers and behavioral response rules** applied on the pass-through path - **Bounded body buffering** with replay for downstream handlers - **Fail-closed on engine malfunction** - a broken engine never turns into an open gate @@ -152,13 +152,13 @@ ___ ## Install -Released: `v1.5.0` on the Go module proxy, flooring the guard-core-go v4.4.0 reference-alignment engine: +Echo v5 support is the `/v2` module (v2.0.0), on guard-core-go v4.4.1. Echo v4 users stay on v1.x (`go get github.com/rennf93/echo-guard@v1.5.0`): ``` -go get github.com/rennf93/echo-guard@v1.5.0 github.com/rennf93/guard-core-go/v4@v4.4.0 +go get github.com/rennf93/echo-guard/v2@v2.0.0 github.com/rennf93/guard-core-go/v4@v4.4.1 ``` -The package name is `echo`, which collides with `github.com/labstack/echo/v4` (also package `echo`), so import the adapter with an explicit alias such as `guardecho`. +The package name is `echo`, which collides with `github.com/labstack/echo/v5` (also package `echo`), so import the adapter with an explicit alias such as `guardecho`. ## Usage @@ -168,9 +168,9 @@ package main import ( "log" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" - guardecho "github.com/rennf93/echo-guard" + guardecho "github.com/rennf93/echo-guard/v2" ) func main() { @@ -190,7 +190,7 @@ func main() { router := echolib.New() router.Use(guard) - router.GET("/", func(c echolib.Context) error { + router.GET("/", func(c *echolib.Context) error { return c.String(200, "ok") }) @@ -206,7 +206,7 @@ Echo's middleware contract has no explicit abort call: a verdict is written to ` ## Development -The middleware consumes the core as a normal module dependency, currently pinned to the guard-core-go master surface (`v4.0.5-0.20260926230539-e39ac203568b`, the behavior-rules / geo-lifecycle / route-detection-exclusions wave); no `replace` directive is used or needed. For cross-repo work on the core itself, add a temporary local `replace` line in your own checkout and drop it before committing. +The middleware consumes the core as a normal module dependency, currently pinned to guard-core-go `v4.4.1`; no `replace` directive is used or needed. For cross-repo work on the core itself, add a temporary local `replace` line in your own checkout and drop it before committing. Integration tests run against real Redis: diff --git a/docs/index.md b/docs/index.md index 0d0e09a..a41bc76 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,12 +14,12 @@ stops because the middleware returns without calling `next`. ## Installation ```bash -go get github.com/rennf93/echo-guard@v1.1.0 github.com/rennf93/guard-core-go/v4@v4.0.5-0.20260926230539-e39ac203568b +go get github.com/rennf93/echo-guard/v2@v2.0.0 github.com/rennf93/guard-core-go/v4@v4.4.1 ``` -Requires Go 1.25 or later. +Requires Go 1.25 or later and Echo v5 (`github.com/labstack/echo/v5`). Echo v4 users stay on `github.com/rennf93/echo-guard` v1.x. -The package name is `echo`, which collides with `github.com/labstack/echo/v4` +The package name is `echo`, which collides with `github.com/labstack/echo/v5` (also package `echo`), so import the adapter with an explicit alias such as `guardecho`. @@ -31,9 +31,9 @@ package main import ( "log" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" - guardecho "github.com/rennf93/echo-guard" + guardecho "github.com/rennf93/echo-guard/v2" ) func main() { @@ -53,7 +53,7 @@ func main() { router := echolib.New() router.Use(guard) - router.GET("/", func(c echolib.Context) error { + router.GET("/", func(c *echolib.Context) error { return c.String(200, "ok") }) diff --git a/docs/usage.md b/docs/usage.md index a084ee4..9dc68af 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -48,7 +48,7 @@ routeIDs := map[string]string{ // Keys are echo route patterns (c.Path()), so parameterized routes map // cleanly. router.Use(func(next echolib.HandlerFunc) echolib.HandlerFunc { - return func(c echolib.Context) error { + return func(c *echolib.Context) error { if routeID, ok := routeIDs[c.Path()]; ok { c.SetRequest(c.Request().WithContext(guardecho.WithRouteID(c.Request().Context(), routeID))) } diff --git a/examples/advanced_app/README.md b/examples/advanced_app/README.md index e83150d..646c920 100644 --- a/examples/advanced_app/README.md +++ b/examples/advanced_app/README.md @@ -93,7 +93,7 @@ curl -s -X POST http://localhost:8080/admin/unban -H 'X-Admin-Token: admin-token `TrustXForwardedProto` in `internal/config`. None are set here because this compose stack has no reverse proxy in front. - The package name is `echo`, which collides with - `github.com/labstack/echo/v4`, so the adapter is imported with an explicit + `github.com/labstack/echo/v5`, so the adapter is imported with an explicit alias (`guardecho`). ## Configuration knobs demonstrated @@ -153,7 +153,7 @@ curl -s -X POST http://localhost:8080/admin/unban -H 'X-Admin-Token: admin-token ## Module layout note Both example apps live inside the root module -(`github.com/rennf93/echo-guard/examples/...`) rather than in separate Go +(`github.com/rennf93/echo-guard/v2/examples/...`) rather than in separate Go modules or a `go.work` workspace. Rationale: the examples pin the exact adapter they document (same module, same commit), so `go vet ./...` and `go build ./...` gate them together with the adapter in CI and the diff --git a/examples/advanced_app/cmd/server/main.go b/examples/advanced_app/cmd/server/main.go index a271ab9..68aa365 100644 --- a/examples/advanced_app/cmd/server/main.go +++ b/examples/advanced_app/cmd/server/main.go @@ -13,13 +13,13 @@ import ( "syscall" "time" - echolib "github.com/labstack/echo/v4" - echomiddleware "github.com/labstack/echo/v4/middleware" - guardecho "github.com/rennf93/echo-guard" + echolib "github.com/labstack/echo/v5" + echomiddleware "github.com/labstack/echo/v5/middleware" + guardecho "github.com/rennf93/echo-guard/v2" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" - "github.com/rennf93/echo-guard/examples/advanced_app/internal/config" - "github.com/rennf93/echo-guard/examples/advanced_app/internal/routes" + "github.com/rennf93/echo-guard/v2/examples/advanced_app/internal/config" + "github.com/rennf93/echo-guard/v2/examples/advanced_app/internal/routes" ) func main() { @@ -78,7 +78,6 @@ func main() { } router := echolib.New() - router.HideBanner = true // Recovery stays outermost so handler panics are contained; the mapper // and the guard follow so blocked requests never reach another handler. router.Use(echomiddleware.Recover()) @@ -118,7 +117,7 @@ func main() { // route pattern before the guard runs. func attachRouteIDs(routeIDs map[string]string) echolib.MiddlewareFunc { return func(next echolib.HandlerFunc) echolib.HandlerFunc { - return func(c echolib.Context) error { + return func(c *echolib.Context) error { if routeID, ok := routeIDs[c.Path()]; ok { c.SetRequest(c.Request().WithContext(guardecho.WithRouteID(c.Request().Context(), routeID))) } diff --git a/examples/advanced_app/internal/routes/routes.go b/examples/advanced_app/internal/routes/routes.go index 005be57..65e0f58 100644 --- a/examples/advanced_app/internal/routes/routes.go +++ b/examples/advanced_app/internal/routes/routes.go @@ -9,7 +9,7 @@ import ( "log" "net/http" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -40,18 +40,18 @@ func Register(e *echolib.Echo, app *App) { // health and ready are excluded from the pipeline (config.ExcludePaths), so // probes and orchestrator health checks never trip the guard. -func (a *App) health(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{"status": "ok"}) +func (a *App) health(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{"status": "ok"}) } -func (a *App) ready(c echolib.Context) error { +func (a *App) ready(c *echolib.Context) error { // Extend this with real dependency probes (Redis PING, cloud range // warmup) for your deployment. - return c.JSON(http.StatusOK, echolib.Map{"status": "ready"}) + return c.JSON(http.StatusOK, map[string]any{"status": "ready"}) } -func (a *App) info(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{ +func (a *App) info(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{ "app": "echo-guard advanced example", "routes": []string{"/health", "/ready", "/echo", "/rate/burst", "/admin/*", "/test/*"}, "engine": "guardcore", @@ -61,12 +61,12 @@ func (a *App) info(c echolib.Context) error { // echo proves the adapter's body contract: the engine scans a bounded prefix // and the replay wrapper hands the handler the full, untouched body. -func (a *App) echo(c echolib.Context) error { +func (a *App) echo(c *echolib.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { - return c.JSON(http.StatusInternalServerError, echolib.Map{"error": "unreadable body"}) + return c.JSON(http.StatusInternalServerError, map[string]any{"error": "unreadable body"}) } - return c.JSON(http.StatusOK, echolib.Map{ + return c.JSON(http.StatusOK, map[string]any{ "echo": true, "method": c.Request().Method, "path": c.Request().URL.Path, @@ -76,8 +76,8 @@ func (a *App) echo(c echolib.Context) error { // burst is limited by config.EndpointRateLimits (5 requests per 60 seconds), // which this port enforces by exact request path. -func (a *App) burst(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{ +func (a *App) burst(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{ "endpoint": "/rate/burst", "limit": "5 requests per 60 seconds", }) @@ -87,21 +87,21 @@ func (a *App) burst(c echolib.Context) error { // RequiredHeaders guard (see cmd/server/main.go): the engine itself rejects // calls missing the admin token with a 400 before these handlers run. -func (a *App) bannedCount(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{ +func (a *App) bannedCount(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{ "banned_ips": a.Engine.Ban.BannedIPCount(), "banned_networks": a.Engine.Ban.BannedNetworkCount(), }) } -func (a *App) ban(c echolib.Context) error { +func (a *App) ban(c *echolib.Context) error { var body struct { IP string `json:"ip"` Seconds int `json:"seconds"` Reason string `json:"reason"` } if err := c.Bind(&body); err != nil || body.IP == "" { - return c.JSON(http.StatusBadRequest, echolib.Map{ + return c.JSON(http.StatusBadRequest, map[string]any{ "error": `body must be {"ip": ..., "seconds": ..., "reason": ...}`, }) } @@ -114,25 +114,25 @@ func (a *App) ban(c echolib.Context) error { created, err := a.Engine.Ban.Ban(body.IP, body.Seconds, body.Reason) if err != nil { log.Printf("admin ban %s failed: %v", body.IP, err) - return c.JSON(http.StatusInternalServerError, echolib.Map{"error": "ban failed"}) + return c.JSON(http.StatusInternalServerError, map[string]any{"error": "ban failed"}) } - return c.JSON(http.StatusOK, echolib.Map{ + return c.JSON(http.StatusOK, map[string]any{ "ip": body.IP, "seconds": body.Seconds, "created": created, }) } -func (a *App) unban(c echolib.Context) error { +func (a *App) unban(c *echolib.Context) error { var body struct { IP string `json:"ip"` } if err := c.Bind(&body); err != nil || body.IP == "" { - return c.JSON(http.StatusBadRequest, echolib.Map{"error": `body must be {"ip": ...}`}) + return c.JSON(http.StatusBadRequest, map[string]any{"error": `body must be {"ip": ...}`}) } if err := a.Engine.Ban.Unban(body.IP); err != nil { log.Printf("admin unban %s failed: %v", body.IP, err) - return c.JSON(http.StatusInternalServerError, echolib.Map{"error": "unban failed"}) + return c.JSON(http.StatusInternalServerError, map[string]any{"error": "unban failed"}) } - return c.JSON(http.StatusOK, echolib.Map{"ip": body.IP, "status": "unbanned"}) + return c.JSON(http.StatusOK, map[string]any{"ip": body.IP, "status": "unbanned"}) } // attackEcho handlers intentionally echo hostile payloads; the guard blocks @@ -140,6 +140,6 @@ func (a *App) unban(c echolib.Context) error { // was bypassed (defense in depth: do not log or store the payload). // Payloads ride in query parameters because this port does not scan request // bodies yet (see the engine port's roadmap notes). -func (a *App) attackEcho(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{"detected": false}) +func (a *App) attackEcho(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{"detected": false}) } diff --git a/examples/simple_app/README.md b/examples/simple_app/README.md index 9dcdbbb..f41cd2b 100644 --- a/examples/simple_app/README.md +++ b/examples/simple_app/README.md @@ -78,7 +78,7 @@ Inline comments in `main.go` walk through every knob used: ## Echo notes - Import the adapter with an explicit alias: the package name is `echo`, - which collides with `github.com/labstack/echo/v4` (also package `echo`); + which collides with `github.com/labstack/echo/v5` (also package `echo`); `guardecho` is the idiomatic alias. - `guardecho.New(engine)` returns an `echo.MiddlewareFunc`, so it composes with `e.Use` and can sit anywhere in a chain. Keep it as early as possible @@ -101,7 +101,7 @@ Inline comments in `main.go` walk through every knob used: ## Module layout note Both example apps live inside the root module -(`github.com/rennf93/echo-guard/examples/...`) rather than in separate Go +(`github.com/rennf93/echo-guard/v2/examples/...`) rather than in separate Go modules or a `go.work` workspace. Rationale: the examples pin the exact adapter they document (same module, same commit), so `go vet ./...` and `go build ./...` gate them together with the adapter in CI and the diff --git a/examples/simple_app/main.go b/examples/simple_app/main.go index c805113..046d194 100644 --- a/examples/simple_app/main.go +++ b/examples/simple_app/main.go @@ -11,15 +11,18 @@ package main import ( + "context" "io" "log" "net/http" "os" + "os/signal" "strings" + "syscall" - echolib "github.com/labstack/echo/v4" - echomiddleware "github.com/labstack/echo/v4/middleware" - guardecho "github.com/rennf93/echo-guard" + echolib "github.com/labstack/echo/v5" + echomiddleware "github.com/labstack/echo/v5/middleware" + guardecho "github.com/rennf93/echo-guard/v2" guardcore "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -129,7 +132,6 @@ func main() { } router := echolib.New() - router.HideBanner = true // Recovery stays outermost so handler panics are contained; the guard // runs next so blocked requests never reach another middleware. Echo has // no explicit abort call: a verdict is written to c.Response() and the @@ -143,23 +145,28 @@ func main() { router.GET("/search", search) log.Println("simple_app listening on :8080") - log.Fatal(router.Start(":8080")) + sc := echolib.StartConfig{Address: ":8080", HideBanner: true} + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + if err := sc.Start(ctx, router); err != nil { + log.Fatal(err) + } } -func info(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{ +func info(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{ "app": "echo-guard simple_app", "endpoints": []string{"/health", "/echo (POST)", "/rate/strict", "/search?q="}, }) } -func health(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{"status": "ok"}) +func health(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{"status": "ok"}) } // echo proves the adapter's body contract: the engine scans a bounded prefix // and the replay wrapper hands the handler the full, untouched body. -func echo(c echolib.Context) error { +func echo(c *echolib.Context) error { if c.Request().Method != http.MethodPost { return c.String(http.StatusMethodNotAllowed, "method not allowed") } @@ -167,17 +174,17 @@ func echo(c echolib.Context) error { if err != nil { return c.String(http.StatusInternalServerError, "unreadable body") } - return c.JSON(http.StatusOK, echolib.Map{"echo": true, "bytes": len(body)}) + return c.JSON(http.StatusOK, map[string]any{"echo": true, "bytes": len(body)}) } -func strict(c echolib.Context) error { - return c.JSON(http.StatusOK, echolib.Map{ +func strict(c *echolib.Context) error { + return c.JSON(http.StatusOK, map[string]any{ "endpoint": "/rate/strict", "limit": "1 request per 10 seconds", }) } -func search(c echolib.Context) error { +func search(c *echolib.Context) error { q := c.QueryParam("q") if strings.ContainsAny(q, "<'") { // The engine should have blocked requests that reach this handler @@ -185,5 +192,5 @@ func search(c echolib.Context) error { // or log the payload. return c.String(http.StatusForbidden, "Blocked by echo-guard") } - return c.JSON(http.StatusOK, echolib.Map{"query": q, "results": []string{}}) + return c.JSON(http.StatusOK, map[string]any{"query": q, "results": []string{}}) } diff --git a/go.mod b/go.mod index f5d6b51..a70e344 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,18 @@ -module github.com/rennf93/echo-guard +module github.com/rennf93/echo-guard/v2 go 1.26.0 require ( - github.com/labstack/echo/v4 v4.16.0 - github.com/rennf93/guard-core-go/v4 v4.4.0 + github.com/labstack/echo/v5 v5.4.0 + github.com/rennf93/guard-core-go/v4 v4.4.1 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect - github.com/labstack/gommon v0.5.0 // indirect - github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect github.com/oschwald/maxminddb-golang v1.13.1 // indirect github.com/redis/go-redis/v9 v9.23.0 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/atomic v1.12.0 // indirect - golang.org/x/crypto v0.57.0 // indirect golang.org/x/net v0.60.0 // indirect golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.42.0 // indirect diff --git a/go.sum b/go.sum index 1cfbf65..6c05b28 100644 --- a/go.sum +++ b/go.sum @@ -8,34 +8,22 @@ github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= -github.com/labstack/echo/v4 v4.16.0 h1:cFqqpqVNmSVyn4nvsXHp5rU4aVLYG3hx4fGWc3FngBk= -github.com/labstack/echo/v4 v4.16.0/go.mod h1:VHAohjgM63iiTVI6EahEDjtRhQNXCMXFp0TMeIsFuW0= -github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= -github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= -github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= -github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/labstack/echo/v5 v5.4.0 h1:iY674460IvSmUcj7MziL3YrgyTClghz5BpdZoKiGxR4= +github.com/labstack/echo/v5 v5.4.0/go.mod h1:4iEGNQiPPZnkfYpNR/L6fINd3NLiGWUD5+eBotFALas= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/redis/go-redis/v9 v9.23.0 h1:/TTVdJa8BRNsybwwGGnmiYktHNlUXtbOQxlWpHBPL3s= github.com/redis/go-redis/v9 v9.23.0/go.mod h1:EWP2UUk+XDaI1s5nQB5mablZN6vccXlmBQTyfnF87D0= -github.com/rennf93/guard-core-go/v4 v4.4.0 h1:+la36FG7WWr65gCc0Rq6RyIGC4Aiy+w91tQzH1SCndg= -github.com/rennf93/guard-core-go/v4 v4.4.0/go.mod h1:6KcajSAwPjYhYxY48nqdrgihH+sXf0NIt6lGMNpKyaU= +github.com/rennf93/guard-core-go/v4 v4.4.1 h1:tRLMLUGpbD9vNpP5xjPUWFayCrhiS04y66/ypzZhdq0= +github.com/rennf93/guard-core-go/v4 v4.4.1/go.mod h1:6KcajSAwPjYhYxY48nqdrgihH+sXf0NIt6lGMNpKyaU= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE= go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= diff --git a/held_test.go b/held_test.go index f17d70e..a7e7dc9 100644 --- a/held_test.go +++ b/held_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -30,9 +30,9 @@ func TestParityFlushInsideHeldResponseDoesNotPanic(t *testing.T) { engine := newTestEngine(t, func(c *guardcore.SecurityConfig) { c.CustomResponseModifier = func(resp *guardcore.Response) *guardcore.Response { return resp } }) - rec := serveWith(t, engine, func(c echolib.Context) error { + rec := serveWith(t, engine, func(c *echolib.Context) error { _, _ = c.Response().Write([]byte("part")) - c.Response().Flush() + _ = http.NewResponseController(c.Response()).Flush() return nil }, httptest.NewRequest("GET", "/stream", nil)) if rec.Code != 200 || rec.Body.String() != "part" { @@ -63,8 +63,8 @@ func TestParityHijackedResponseIsNotReleased(t *testing.T) { } router := echolib.New() router.Use(guard) - router.Any("/*", func(c echolib.Context) error { - conn, _, err := c.Response().Hijack() + router.Any("/*", func(c *echolib.Context) error { + conn, _, err := http.NewResponseController(c.Response()).Hijack() if err != nil { return err } diff --git a/integration_test.go b/integration_test.go index fa24ae8..8d7a567 100644 --- a/integration_test.go +++ b/integration_test.go @@ -8,7 +8,7 @@ import ( "os" "testing" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -46,7 +46,7 @@ func serveBlocked(t *testing.T, guard echolib.MiddlewareFunc, r *http.Request) * t.Helper() router := echolib.New() router.Use(guard) - router.Any("/*", func(c echolib.Context) error { + router.Any("/*", func(c *echolib.Context) error { t.Fatal("blocked request must not reach the handler") return nil }) @@ -88,7 +88,7 @@ func TestIntegrationMiddlewareStartupWiringAcrossEngines(t *testing.T) { allowed.RemoteAddr = "203.0.113.62:4711" router := echolib.New() router.Use(secondGuard) - router.Any("/*", func(c echolib.Context) error { + router.Any("/*", func(c *echolib.Context) error { return c.NoContent(http.StatusOK) }) rec = httptest.NewRecorder() diff --git a/middleware.go b/middleware.go index 8d18d95..a9f58f5 100644 --- a/middleware.go +++ b/middleware.go @@ -6,7 +6,7 @@ import ( "log" "net/http" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -48,7 +48,7 @@ func New(engine *guardcore.Engine, opts ...Option) (echolib.MiddlewareFunc, erro } func (m *middleware) wrap(next echolib.HandlerFunc) echolib.HandlerFunc { - return func(c echolib.Context) error { + return func(c *echolib.Context) error { req := newRequestShim(c, m.maxBytes) verdict, err := m.check(req) if err != nil { @@ -83,25 +83,34 @@ func (m *middleware) wrap(next echolib.HandlerFunc) echolib.HandlerFunc { // CustomResponseModifier configured the whole response is held // instead, so the modifier (the last step of the reference // process_response) can rewrite it before it is sent. - origWriter := c.Response().Writer + // Echo v5 hands out an http.ResponseWriter: the writers are swapped + // underneath the echo.Response, as v4 did through Response.Writer, so + // its Status and Committed bookkeeping keeps working. + echoResponse, err := echolib.UnwrapResponse(c.Response()) + if err != nil { + m.logger.Printf("guardcore echo: response writer is not an echo response, failing closed: %v", err) + applyResponse(c, m.engine.CreateErrorResponse(500, failClosedMessage)) + return nil + } + origWriter := echoResponse.ResponseWriter var held *heldWriter if m.engine.Config.CustomResponseModifier != nil { held = &heldWriter{ResponseWriter: origWriter} - c.Response().Writer = held + echoResponse.ResponseWriter = held } capture := m.newBodyCapture() if capture != nil && held == nil { - c.Response().Writer = capture.wrap(origWriter) + echoResponse.ResponseWriter = capture.wrap(origWriter) } err = next(c) // A handler error is turned into a response by Echo's // HTTPErrorHandler, which normally runs after the whole middleware - // chain returned: c.Response().Status would still be the unwritten + // chain returned: echoResponse.Status would still be the unwritten // default and the engine would record a 200. Run the error handler // here, as Echo's own logger middleware does, so the real status // (and the held body, when a modifier is configured) exists first. if err != nil { - c.Error(err) + m.handleError(c, err) } var observedBody []byte if capture != nil { @@ -117,15 +126,15 @@ func (m *middleware) wrap(next echolib.HandlerFunc) echolib.HandlerFunc { observedBody = observedBody[:capture.budget] } } - c.Response().Writer = origWriter + echoResponse.ResponseWriter = origWriter } m.engine.ProcessResponse(req, &guardcore.Response{ - StatusCode: c.Response().Status, + StatusCode: echoResponse.Status, Headers: map[string]string{}, Body: observedBody, }) if held != nil { - held.release(m.engine, c.Response().Status, cors) + held.release(m.engine, echoResponse.Status, cors) } return nil } @@ -174,6 +183,20 @@ func (w *captureWriter) Write(p []byte) (int, error) { return w.ResponseWriter.Write(p) } +// handleError is Echo v4's c.Error: v5 dropped the method, the error handler +// is reached through the Echo instance (the pattern echo's request logger +// middleware uses). v5's default handler cannot answer without an Echo +// instance (no JSON serializer), so such a context fails closed with the +// engine's 500 response. +func (m *middleware) handleError(c *echolib.Context, err error) { + if app := c.Echo(); app != nil && app.HTTPErrorHandler != nil { + app.HTTPErrorHandler(c, err) + return + } + m.logger.Printf("guardcore echo: no echo error handler for handler error, failing closed: %v", err) + applyResponse(c, m.engine.CreateErrorResponse(500, failClosedMessage)) +} + func (m *middleware) check(req guardcore.Request) (verdict *guardcore.Response, err error) { defer func() { if r := recover(); r != nil { @@ -183,7 +206,7 @@ func (m *middleware) check(req guardcore.Request) (verdict *guardcore.Response, return m.engine.Check(req), nil } -func applyResponse(c echolib.Context, response *guardcore.Response) { +func applyResponse(c *echolib.Context, response *guardcore.Response) { for name, value := range response.Headers { c.Response().Header().Set(name, value) } diff --git a/middleware_test.go b/middleware_test.go index 785ce57..9cef67b 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -2,6 +2,7 @@ package echo import ( "bytes" + "errors" "io" "log" "net/http" @@ -9,16 +10,12 @@ import ( "strings" "testing" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) const xssVector = "q=" -func init() { - echolib.New().Logger.SetOutput(io.Discard) -} - type probe struct { called bool headers http.Header @@ -60,7 +57,7 @@ func serve(t *testing.T, guard echolib.MiddlewareFunc, r *http.Request) (*httpte p := &probe{} router := echolib.New() router.Use(guard) - router.Any("/*", func(c echolib.Context) error { + router.Any("/*", func(c *echolib.Context) error { p.called = true p.headers = c.Response().Header().Clone() p.body, _ = io.ReadAll(c.Request().Body) @@ -74,7 +71,7 @@ func serve(t *testing.T, guard echolib.MiddlewareFunc, r *http.Request) (*httpte return rec, p } -func newShimContext(r *http.Request) echolib.Context { +func newShimContext(r *http.Request) *echolib.Context { return echolib.New().NewContext(r, httptest.NewRecorder()) } @@ -522,3 +519,36 @@ func TestMiddlewareInvalidOptionsIgnored(t *testing.T) { t.Fatalf("middleware with defaults must pass clean traffic, got %d called=%v", rec.Code, p.called) } } + +// Echo v5 exposes the response as a plain http.ResponseWriter: a context whose +// writer chain never reaches an echo.Response cannot be observed, so the +// middleware fails closed instead of letting the handler run unobserved. +type opaqueWriter struct{ http.ResponseWriter } + +func TestMiddlewareFailsClosedWithoutEchoResponse(t *testing.T) { + guard, _ := newTestMiddleware(t, nil) + rec := httptest.NewRecorder() + c := echolib.NewContext(httptest.NewRequest("GET", "/api", nil), rec) + c.SetResponse(opaqueWriter{rec}) + called := false + if err := guard(func(*echolib.Context) error { called = true; return nil })(c); err != nil { + t.Fatalf("fail-closed must not surface an error, got %v", err) + } + if called || rec.Code != 500 || rec.Body.String() != failClosedMessage { + t.Fatalf("expected fail-closed 500 without calling the handler, got %d %q called=%v", rec.Code, rec.Body.String(), called) + } +} + +// A context built without an Echo instance (echolib.NewContext) has no +// HTTPErrorHandler: a handler error fails closed with the engine's 500. +func TestMiddlewareHandlerErrorWithoutEchoInstanceFailsClosed(t *testing.T) { + guard, _ := newTestMiddleware(t, nil) + rec := httptest.NewRecorder() + c := echolib.NewContext(httptest.NewRequest("GET", "/api", nil), rec) + if err := guard(func(*echolib.Context) error { return errors.New("boom") })(c); err != nil { + t.Fatalf("handler errors are turned into a response, got %v", err) + } + if rec.Code != 500 || rec.Body.String() != failClosedMessage { + t.Fatalf("expected the fail-closed 500, got %d %q", rec.Code, rec.Body.String()) + } +} diff --git a/parity_test.go b/parity_test.go index e2fdd32..a0dd510 100644 --- a/parity_test.go +++ b/parity_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -39,7 +39,7 @@ func TestParityModifierRewritesPassThroughResponse(t *testing.T) { return resp } }) - rec := serveWith(t, engine, func(c echolib.Context) error { + rec := serveWith(t, engine, func(c *echolib.Context) error { c.Response().Header().Add("Set-Cookie", "a=1") c.Response().Header().Add("Set-Cookie", "b=2") c.Response().Header().Set("Content-Length", "5") @@ -83,7 +83,7 @@ func TestParityModifierRunsAfterCORSAndCORSIsReapplied(t *testing.T) { }) r := httptest.NewRequest("GET", "/api", nil) r.Header.Set("Origin", "https://app.example") - rec := serveWith(t, engine, func(c echolib.Context) error { return c.String(200, "ok") }, r) + rec := serveWith(t, engine, func(c *echolib.Context) error { return c.String(200, "ok") }, r) if rec.Header().Get("X-Saw-CORS") != "yes" { t.Fatalf("the modifier must see the CORS headers: %v", rec.Header()) } @@ -99,7 +99,7 @@ func TestParityModifierAppliesToGuardBlockedResponse(t *testing.T) { return resp } }) - rec := serveWith(t, engine, func(c echolib.Context) error { return c.String(200, "ok") }, + rec := serveWith(t, engine, func(c *echolib.Context) error { return c.String(200, "ok") }, httptest.NewRequest("GET", "/api?"+xssVector, nil)) if rec.Code != 400 || rec.Header().Get("X-Modified") != "yes" { t.Fatalf("guard responses carry the modifier, got %d %v", rec.Code, rec.Header()) @@ -116,7 +116,7 @@ func TestParityModifierStatusAndBodyFeedReturnRules(t *testing.T) { } }) for i := 0; i < 2; i++ { - rec := serveWith(t, engine, func(c echolib.Context) error { return c.String(200, "a secret leaked here") }, + rec := serveWith(t, engine, func(c *echolib.Context) error { return c.String(200, "a secret leaked here") }, httptest.NewRequest("GET", "/report", nil)) if rec.Code != 200 || rec.Body.String() != "a secret leaked here" { t.Fatalf("held response must reach the client intact, got %d %q", rec.Code, rec.Body.String()) @@ -139,7 +139,7 @@ func TestParityHeldBodyBeyondBudgetIsNotScanned(t *testing.T) { big := []byte(strings.Repeat("a", 2048)) copy(big[1500:], "beyond-the-budget") for i := 0; i < 3; i++ { - rec := serveWith(t, engine, func(c echolib.Context) error { _, err := c.Response().Write(big); return err }, + rec := serveWith(t, engine, func(c *echolib.Context) error { _, err := c.Response().Write(big); return err }, httptest.NewRequest("GET", "/report", nil)) if rec.Code != 200 || rec.Body.Len() != 2048 { t.Fatalf("the client must get the whole body, got %d with %d bytes", rec.Code, rec.Body.Len()) @@ -156,9 +156,9 @@ func TestParityHeldBodyBeyondBudgetIsNotScanned(t *testing.T) { // status instead of the not-yet-written default. func TestParityHandlerErrorStatusReachesEngine(t *testing.T) { for name, handler := range map[string]echolib.HandlerFunc{ - "HTTPError": func(c echolib.Context) error { return echolib.NewHTTPError(http.StatusInternalServerError, "boom") }, - "plain error": func(c echolib.Context) error { return errors.New("boom") }, - "explicit 500": func(c echolib.Context) error { return c.NoContent(http.StatusInternalServerError) }, + "HTTPError": func(c *echolib.Context) error { return echolib.NewHTTPError(http.StatusInternalServerError, "boom") }, + "plain error": func(c *echolib.Context) error { return errors.New("boom") }, + "explicit 500": func(c *echolib.Context) error { return c.NoContent(http.StatusInternalServerError) }, } { t.Run(name, func(t *testing.T) { engine := newTestEngine(t, func(c *guardcore.SecurityConfig) { @@ -186,7 +186,7 @@ func TestParityHandlerErrorWithModifierIsHeldAndModified(t *testing.T) { return resp } }) - rec := serveWith(t, engine, func(c echolib.Context) error { return echolib.NewHTTPError(http.StatusTeapot, "short") }, + rec := serveWith(t, engine, func(c *echolib.Context) error { return echolib.NewHTTPError(http.StatusTeapot, "short") }, httptest.NewRequest("GET", "/tea", nil)) if rec.Code != http.StatusTeapot || rec.Header().Get("X-Seen-Status") != "I'm a teapot" { t.Fatalf("the modifier must see the error status, got %d %v", rec.Code, rec.Header()) @@ -202,7 +202,7 @@ func TestParityAddressHeadersNeedNoManualExclusion(t *testing.T) { r.Host = "169.254.169.254" r.Header.Set("X-Forwarded-For", "10.0.0.1") r.Header.Set("Origin", "http://localhost") - rec := serveWith(t, engine, func(c echolib.Context) error { return c.String(200, "ok") }, r) + rec := serveWith(t, engine, func(c *echolib.Context) error { return c.String(200, "ok") }, r) if rec.Code != 200 { t.Fatalf("address headers must not be flagged by default, got %d %q", rec.Code, rec.Body.String()) } diff --git a/reachability_test.go b/reachability_test.go index d20226d..bb37574 100644 --- a/reachability_test.go +++ b/reachability_test.go @@ -13,7 +13,7 @@ import ( "strings" "testing" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -39,7 +39,7 @@ func serveWithRouteID(t *testing.T, engine *guardcore.Engine, routeID, target st } router := echolib.New() router.Use(func(next echolib.HandlerFunc) echolib.HandlerFunc { - return func(c echolib.Context) error { + return func(c *echolib.Context) error { if routeID != "" { c.SetRequest(c.Request().WithContext(WithRouteID(c.Request().Context(), routeID))) } @@ -47,7 +47,7 @@ func serveWithRouteID(t *testing.T, engine *guardcore.Engine, routeID, target st } }) router.Use(guard) - router.Any("/*", func(c echolib.Context) error { + router.Any("/*", func(c *echolib.Context) error { body := "handler-body" if len(handlerBody) > 0 { body = handlerBody[0] @@ -127,7 +127,7 @@ func TestReachabilityGlobalReturnPatternBodyScan(t *testing.T) { } router := echolib.New() router.Use(guard) - router.Any("/*", func(c echolib.Context) error { + router.Any("/*", func(c *echolib.Context) error { big := make([]byte, 2048) for i := range big { big[i] = 'a' @@ -164,7 +164,7 @@ func TestReachabilityStatusReturnPatternWithoutScan(t *testing.T) { // The handler writes the 404 itself (instead of returning an Echo // HTTPError) so the adapter observes the status while the middleware // still runs; see the docs note on error-returning handlers. - router.Any("/*", func(c echolib.Context) error { + router.Any("/*", func(c *echolib.Context) error { c.Response().WriteHeader(http.StatusNotFound) _, _ = c.Response().Write([]byte("missing")) return nil @@ -267,7 +267,7 @@ func TestReachabilityCORS(t *testing.T) { }) router := echolib.New() router.Use(guard) - router.Any("/*", func(c echolib.Context) error { + router.Any("/*", func(c *echolib.Context) error { _, _ = c.Response().Write([]byte("ok")) return nil }) diff --git a/request.go b/request.go index db74c24..cfe30f2 100644 --- a/request.go +++ b/request.go @@ -9,7 +9,7 @@ import ( "strings" "sync" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -35,7 +35,7 @@ type requestShim struct { var _ guardcore.Request = (*requestShim)(nil) -func newRequestShim(c echolib.Context, maxBodyBytes int64) *requestShim { +func newRequestShim(c *echolib.Context, maxBodyBytes int64) *requestShim { if maxBodyBytes <= 0 { maxBodyBytes = DefaultMaxBodyBytes } diff --git a/status.go b/status.go index caa41fc..fa4e2db 100644 --- a/status.go +++ b/status.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -124,7 +124,7 @@ func NewStatusHandler(engine *guardcore.Engine) (echolib.HandlerFunc, error) { if engine == nil { return nil, errors.New("engine must not be nil") } - return func(c echolib.Context) error { + return func(c *echolib.Context) error { return c.JSON(http.StatusOK, initializationStatus(engine)) }, nil } diff --git a/status_test.go b/status_test.go index 6f808c9..290a54c 100644 --- a/status_test.go +++ b/status_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) diff --git a/websocket.go b/websocket.go index 36d81a2..4bd92e9 100644 --- a/websocket.go +++ b/websocket.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -26,7 +26,7 @@ import ( // the penetration detection pass sharing the HTTP pipeline's suspicious // counts. A nil result allows the upgrade; a non-nil reason closes it. A // nil engine fails closed with the security-check-failed reason. -func GuardWebSocket(engine *guardcore.Engine, c echolib.Context) *guardcore.WebSocketCloseReason { +func GuardWebSocket(engine *guardcore.Engine, c *echolib.Context) *guardcore.WebSocketCloseReason { if engine == nil || c == nil { reason := guardcore.WSCloseSecurityCheckFailed return &reason diff --git a/websocket_test.go b/websocket_test.go index fe09a7d..db8411e 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - echolib "github.com/labstack/echo/v4" + echolib "github.com/labstack/echo/v5" "github.com/rennf93/guard-core-go/v4/guardcore" ) @@ -28,7 +28,7 @@ func wsTestEngine(t *testing.T, mutate func(*guardcore.SecurityConfig)) *guardco return engine } -func wsUpgradeContext(t *testing.T, remote string, mutate func(r *http.Request)) echolib.Context { +func wsUpgradeContext(t *testing.T, remote string, mutate func(r *http.Request)) *echolib.Context { t.Helper() r := httptest.NewRequest(http.MethodGet, "/ws", nil) r.RemoteAddr = remote