1 Commits
Author SHA1 Message Date
sneak 1b949fddc6 Rename internal/proxy to internal/servewaf (closes #173)
check / check (push) Waiting to run
The package passes nothing to an app since rproxy took that over, so it
is named after the command it serves. Its constructor is New, like
rproxy's. The rproxy tests call their stand-in for servewaf standIn,
since a local servewaf would hide the package. No change of behaviour.

Model: opus-5-5
2026-11-06 18:29:22 +00:00
54 changed files with 545 additions and 1031 deletions
+2 -6
View File
@@ -1,13 +1,12 @@
.PHONY: bootstrap setup test lint fmt fmt-check tidy check docker hooks build \ .PHONY: bootstrap setup test lint fmt fmt-check tidy check docker hooks build \
run example-app gitea-requests gitea-setup run example-app gitea-requests
# Makefile targets are thin shims; the implementations live in script/ # Makefile targets are thin shims; the implementations live in script/
# per the scripts-to-rule-them-all pattern (see the Entrypoints section # per the scripts-to-rule-them-all pattern (see the Entrypoints section
# of README.md). tidy writes go.mod and go.sum as `go mod tidy` does, # of README.md). tidy writes go.mod and go.sum as `go mod tidy` does,
# which test checks. build and run are for working on the code by hand; # which test checks. build and run are for working on the code by hand;
# example-app checks the image with an app built on it; gitea-requests # example-app checks the image with an app built on it; gitea-requests
# reports the gitea requests the Core Rule Set refuses at the defaults; # reports the gitea requests the Core Rule Set refuses at the defaults.
# gitea-setup checks the setup in deploy/gitea.
bootstrap: bootstrap:
@script/bootstrap @script/bootstrap
@@ -50,6 +49,3 @@ example-app:
gitea-requests: gitea-requests:
@script/gitea-requests @script/gitea-requests
gitea-setup:
@script/gitea-setup
+5 -11
View File
@@ -511,9 +511,8 @@ volumes:
servewaf-state: servewaf-state:
``` ```
`deploy/gitea` holds a complete setup of gitea, traefik included, as a Docker [`docs/gitea.md`](docs/gitea.md) records what the defaults do in front of gitea,
Compose file. [`docs/gitea.md`](docs/gitea.md) explains it, and records what the request by request.
defaults do in front of gitea, request by request.
### `rproxy` beside the app ### `rproxy` beside the app
@@ -689,8 +688,8 @@ aside as `<name>.bad`, and never stops the running process (see
health check. health check.
- `internal/config`: reads the settings, the one place they are read. - `internal/config`: reads the settings, the one place they are read.
- `internal/rproxy`: the reverse proxy in front of one app. - `internal/rproxy`: the reverse proxy in front of one app.
- `internal/proxy`: `servewaf`'s server, which runs the checks and answers its - `internal/servewaf`: `servewaf`'s server, which runs the checks and answers
own endpoints. its own endpoints.
- `internal/ratelimit`, `internal/bans`, `internal/rules`, `internal/waf`, - `internal/ratelimit`, `internal/bans`, `internal/rules`, `internal/waf`,
`internal/lookup`, `internal/reputation`, `internal/anomaly` and `internal/lookup`, `internal/reputation`, `internal/anomaly` and
`internal/alerts`: one check or source each. `internal/alerts`: one check or source each.
@@ -786,10 +785,6 @@ on the host.
Core Rule Set is known to refuse, and prints what `servewaf` did with each. Core Rule Set is known to refuse, and prints what `servewaf` did with each.
Needs network access, and a docker daemon that pushes to a registry on Needs network access, and a docker daemon that pushes to a registry on
`localhost` over plain HTTP. `make gitea-requests`. `localhost` over plain HTTP. `make gitea-requests`.
- `script/gitea-setup`: builds the image, brings up the setup in `deploy/gitea`,
and checks it as [`docs/gitea.md`](docs/gitea.md) describes. Then it removes
the containers, the network, the volumes and the image it built. Needs network
access. `make gitea-setup`.
## TODO ## TODO
@@ -813,8 +808,7 @@ on the host.
- [`docs/state-files.md`](docs/state-files.md): the state files and what each - [`docs/state-files.md`](docs/state-files.md): the state files and what each
holds. holds.
- [`docs/image.md`](docs/image.md): what the image holds and how it is pinned. - [`docs/image.md`](docs/image.md): what the image holds and how it is pinned.
- [`docs/gitea.md`](docs/gitea.md): `smallwebwaf` in front of gitea: the setup - [`docs/gitea.md`](docs/gitea.md): `smallwebwaf` in front of gitea.
in `deploy/gitea`, and what the defaults do there.
## License ## License
+4 -4
View File
@@ -66,10 +66,10 @@ to them.
one it does not know, the binary prints the commands and exits with one it does not know, the binary prints the commands and exits with
status 2. status 2.
- `internal/config`: reads the settings, the one place they are read. - `internal/config`: reads the settings, the one place they are read.
- `internal/proxy`: `servewaf`'s server. It runs the checks in `check`, and - `internal/servewaf`: `servewaf`'s server. It runs the checks in `check`,
holds `servewaf`'s question, answer and report. It answers `servewaf`'s and holds `servewaf`'s question, answer and report. It answers
own endpoints in `answerOwnEndpoint`. It counts bytes, refusals and `servewaf`'s own endpoints in `answerOwnEndpoint`. It counts bytes,
anomalies, and writes the log lines. refusals and anomalies, and writes the log lines.
- `internal/rproxy`: the reverse proxy in front of one app that asks - `internal/rproxy`: the reverse proxy in front of one app that asks
`servewaf` about each request. It passes a request to the app with the `servewaf` about each request. It passes a request to the app with the
standard library's `httputil.ReverseProxy`, within the time and size standard library's `httputil.ReverseProxy`, within the time and size
-135
View File
@@ -1,135 +0,0 @@
# Gitea behind traefik, with rproxy and servewaf in front of it.
# docs/gitea.md says what each setting here does and why. Before starting it:
# - set SMALLWEBWAF_IMAGE to the smallwebwaf image, named by digest;
# - replace git.example.org with the site's host name;
# - replace each 10.99.0 with the first three numbers of a /24 subnet that
# no other network on the host uses. The /24 must lie inside 10.0.0.0/8,
# 172.16.0.0/12 or 192.168.0.0/16, because rproxy believes
# X-Forwarded-For only from those by default. Replace each fd10:99 with
# the first two hextets of an unused /64 subnet whose third and fourth
# hextets are 0, as in fd10:99::/64;
# - set GITEA_SECRET_KEY to the output of
# `docker run --rm <gitea's image below> gitea generate secret SECRET_KEY`,
# once per install, and keep it.
# Then run `docker compose up --detach` in this directory.
# script/gitea-setup checks this setup.
name: gitea
x-logging: &logging
driver: json-file
options:
max-size: 10m
max-file: "5"
services:
traefik:
# traefik v3.7.17, 2026-11-03
image: traefik@sha256:68ae5fe5b64ca166704b746a9e8afcd385e36b3f569a5691401bf652d57b3748
# traefik replaces the X-Forwarded-For header a visitor sends. Keep it
# so: otherwise a visitor could name an address on the shared network,
# and skip servewaf's checks.
command:
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.websecure.address=:443
# Until a certificate resolver is added here, as for your other
# sites, traefik answers with a certificate it made itself.
- --entrypoints.websecure.http.tls=true
# traefik's own limit of 60 seconds for a whole request would cut
# off an upload long before rproxy's limit of 30 minutes.
- --entrypoints.websecure.transport.respondingTimeouts.readTimeout=30m
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
shared:
# Containers on the shared network find traefik under the
# site's name, and so reach it from their own address.
aliases:
- git.example.org
restart: unless-stopped
gitea:
# gitea 1.27.3, 2026-08-29
image: gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef
environment:
GITEA__server__ROOT_URL: https://git.example.org/
# Gitea starts at its defaults, with SQLite, instead of showing its
# install page to whoever opens it first.
GITEA__security__INSTALL_LOCK: "true"
GITEA__security__SECRET_KEY: ${GITEA_SECRET_KEY:?set GITEA_SECRET_KEY, see docs/gitea.md}
# Accounts are made with `gitea admin user create`.
GITEA__service__DISABLE_REGISTRATION: "true"
# rproxy's addresses. Gitea then takes each visitor's address from
# the X-Real-Ip header traefik sets and rproxy passes on.
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 10.99.0.2,fd10:99::2
volumes:
- gitea-data:/data
networks:
- shared
restart: unless-stopped
rproxy:
image: ${SMALLWEBWAF_IMAGE:?set SMALLWEBWAF_IMAGE to the smallwebwaf image}
environment:
SWRP_WAF_BACKEND: http://servewaf:8080
SWRP_UPSTREAM_URL: http://gitea:3000
# Pushes, LFS objects, packages and release attachments of up to
# 2 GiB, sent in up to 30 minutes.
SWRP_MAX_REQUEST_SIZE: 2G
SWRP_MAX_REQUEST_TIME_MS: "1800000"
# 15 minutes for a clone or a download, half the default.
SWRP_MAX_RESPONSE_TIME_MS: "900000"
labels:
traefik.enable: "true"
traefik.http.routers.gitea.rule: Host(`git.example.org`)
traefik.http.routers.gitea.entrypoints: websecure
traefik.http.services.gitea.loadbalancer.server.port: "8080"
networks:
shared:
ipv4_address: 10.99.0.2
ipv6_address: fd10:99::2
logging: *logging
restart: unless-stopped
servewaf:
image: ${SMALLWEBWAF_IMAGE:?set SMALLWEBWAF_IMAGE to the smallwebwaf image}
environment:
SMALLWEBWAF_COMMAND: servewaf
# The containers on the shared network skip the checks. Add the
# address of an automation fleet on another host here too; without
# that, the default request limits are sized for such a fleet.
SWWAF_ALLOW_NETS: 10.99.0.128/25,fd10:99:0:0:8000::/65
# In block mode the Core Rule Set refuses everyday Gitea searches
# and file names, and 31 refusals in a minute ban the address.
SWWAF_WAF_MODE: detect
volumes:
- servewaf-state:/var/lib/smallwebwaf
networks:
- shared
logging: *logging
restart: unless-stopped
networks:
# The network traefik and Gitea share. Its containers get addresses in the
# upper half of each subnet, which servewaf lets skip the checks. Each
# subnet's gateway, the host's own address on it, stays out of that half:
# requests docker passes on from a published port can come from it.
shared:
enable_ipv6: true
ipam:
config:
- subnet: 10.99.0.0/24
ip_range: 10.99.0.128/25
gateway: 10.99.0.1
- subnet: fd10:99::/64
ip_range: fd10:99:0:0:8000::/65
gateway: fd10:99::1
volumes:
gitea-data:
servewaf-state:
+49 -176
View File
@@ -1,184 +1,51 @@
# smallwebwaf in front of gitea # smallwebwaf in front of gitea
`deploy/gitea/compose.yaml` runs gitea behind traefik, with `rproxy` and These notes record what `smallwebwaf` does in front of gitea at the default
`servewaf` in front of it. This document says what that setup sets and why, and settings. They are observations, not requirements; the requirements are in
what the defaults would do without it. [`SPEC.md`](../SPEC.md) says how [`SPEC.md`](../SPEC.md). `script/gitea-requests` runs gitea behind `rproxy` and
`smallwebwaf` behaves. `servewaf`, and sends through them a sample of gitea's ordinary use and the
## The setup
Four containers share one Docker network, the shared network:
- traefik, which routes the site's HTTPS requests to `rproxy`, and redirects
plain HTTP to HTTPS;
- `rproxy`, in front of gitea;
- gitea 1.27.3, as its image comes;
- `servewaf`, with a volume for its state files.
To start it:
1. In `deploy/gitea/compose.yaml`, replace `git.example.org` with the site's
host name.
2. Replace each `10.99.0` with the first three numbers of a `/24` subnet that no
other network on the host uses. The `/24` must lie inside `10.0.0.0/8`,
`172.16.0.0/12` or `192.168.0.0/16`, because `rproxy` believes
`X-Forwarded-For` only from those by default. Replace each `fd10:99` with the
first two hextets of an unused `/64` subnet whose third and fourth hextets
are 0, as in `fd10:99::/64`.
3. Set `SMALLWEBWAF_IMAGE` to the `smallwebwaf` image, named by digest.
4. Set `GITEA_SECRET_KEY` to the output of
`docker run --rm <gitea's image> gitea generate secret SECRET_KEY`, with
gitea's image as `deploy/gitea/compose.yaml` names it. Do this once per
install, and keep the key.
5. Run `docker compose up --detach` in `deploy/gitea`.
6. Make gitea's first user with `gitea admin user create`, in gitea's container,
as the user `git`.
Sign-up is off: accounts are made with `gitea admin user create`. gitea starts
at its defaults, with SQLite. traefik answers with a certificate it made itself
until you add a certificate resolver, as for your other sites. SSH to gitea does
not pass through `smallwebwaf`; publish gitea's SSH port yourself if you use it.
`make gitea-setup` checks the setup. It builds the image, brings the setup up
under a name of its own, and checks:
- that a page load, a push and a clone sent through traefik pass through
`rproxy`, and that `servewaf` checks them;
- that gitea logs the visitor's address, not `rproxy`'s;
- that a request from a container on the shared network skips the checks;
- that with `servewaf` stopped, gitea still answers through `rproxy`.
## Uploads and downloads
An upload is a request:
- a git push;
- an LFS object;
- a container image layer;
- a package file;
- a release attachment.
The setup lets an upload of up to 2 GiB, sent in up to 30 minutes, through:
- `SWRP_MAX_REQUEST_SIZE` is `2G`. At the default, an upload over 100 MiB is cut
off.
- `SWRP_MAX_REQUEST_TIME_MS` is `1800000`, 30 minutes. At the default, an upload
that takes over 60 seconds is cut off.
- traefik's `readTimeout` is 30 minutes. traefik's own default of 60 seconds for
a whole request would cut off an upload first.
A clone or a download is a response:
- `SWRP_MAX_RESPONSE_TIME_MS` is `900000`, 15 minutes, half the default. A clone
or download that takes longer is cut off.
- `SWRP_MAX_RESPONSE_SIZE` stays at its default, 1 GiB. A repository whose clone
is larger needs it raised, and the byte limits with it ("Byte limits, per
client" in `SPEC.md`).
The byte limits stay at their defaults. Two uploads of 2 GiB from one address in
one minute pass the minute's limit of 4 GiB, and ban the address.
## The checks
`SWWAF_WAF_MODE` is `detect`. The Core Rule Set then logs a match, with its rule
ids, and refuses nothing. In `block` mode, the default, it refuses everyday
gitea searches and file names. The section "What the Core Rule Set refuses in
`block` mode" below lists them. Each such refusal counts toward the error burst,
so 31 in one minute ban the address.
Every other check stays at its default. A client may make 500 requests a minute,
20,000 an hour and 300,000 a day. Archive downloads and blame and history pages
are what scrapers hammer, and these limits do most of the work there.
## The shared network
Containers on the shared network skip `servewaf`'s checks:
- The shared network's subnets are `10.99.0.0/24` and `fd10:99::/64`. Docker
gives its containers addresses in the upper half of each, `10.99.0.128/25` and
`fd10:99:0:0:8000::/65`, which `SWWAF_ALLOW_NETS` names.
- traefik is known on the shared network by the site's host name. So a container
there that asks for the site reaches traefik from its own address. An
automation fleet in such a container skips the checks.
- Each subnet's gateway, `10.99.0.1` and `fd10:99::1`, is the host's own address
on it. Requests docker passes on from a published port can come from the
gateway, so the gateways are left out of `SWWAF_ALLOW_NETS`. The network has
IPv6, so a visitor over IPv6 reaches traefik from its own address, and
`servewaf` checks it as itself.
- A container on the shared network that asks for the site at the host's public
address arrives from the gateway. It is checked like any visitor.
- A fleet on another host can have its address added to `SWWAF_ALLOW_NETS`.
Without that, the default request limits are sized for such a fleet.
- traefik replaces the `X-Forwarded-For` a visitor sends, by default. Keep it
so: otherwise a visitor could name an address on the shared network, and skip
the checks.
- Anything on the shared network can reach gitea and `servewaf` around `rproxy`.
Put only containers you trust on it.
## gitea's settings
- `REVERSE_PROXY_TRUSTED_PROXIES` names `rproxy`'s fixed addresses, `10.99.0.2`
and `fd10:99::2`. gitea then takes each visitor's address from the `X-Real-Ip`
header, which traefik sets and `rproxy` passes on. Without it, gitea logs
every visitor as `rproxy`.
- `INSTALL_LOCK` is `true`, as "The setup" above says.
## Methods
`rproxy` passes on only these methods: `GET`, `HEAD`, `POST`, `OPTIONS`, `PUT`,
`DELETE`, `PATCH` and `QUERY`. gitea 1.27.3 needs no other. Its routes use:
- pages and the API: `GET`, `HEAD`, `POST`, `PUT`, `PATCH` and `DELETE`;
- git's push, clone and fetch over HTTP: `GET`, `POST` and `OPTIONS`;
- LFS: `GET`, `POST` and `PUT`;
- the container registry and the other package registries: `GET`, `HEAD`,
`POST`, `PUT`, `PATCH` and `DELETE`;
- Actions runners: `POST`.
## The log
`rproxy` and `servewaf` write each query string to the log whole. A token sent
in the query string, as `?token=`, is then in the log. Send it in the
`Authorization` header instead.
## Actions runners
A runner on the shared network skips the checks. One on another host is counted
like any client. A runner older than version 0.4 (April 2026) asks for work
every 2 seconds, 43,200 requests a day. While it runs a job, it reports the
job's log and state every second instead, two requests a second. So one runner
busy all day sends 120 requests a minute and 172,800 a day, under the request
limits.
Several runners on one address can pass them:
- Two busy all day make 345,600 requests, and pass the day limit of 300,000.
- Three busy at once make 21,600 an hour, and pass the hour limit of 20,000.
- Five busy at once make 600 a minute, and pass the minute limit of 500.
Their address is then banned, their running jobs fail, and each repeat within a
day makes the ban longer. Newer runners ask for work less often. Put the
runners' addresses in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
## What the Core Rule Set refuses in `block` mode
These notes record what the Core Rule Set does in front of gitea in `block`
mode, at the other defaults. They are observations, not requirements.
`script/gitea-requests` runs gitea behind `rproxy` and `servewaf` at the
defaults. It sends through them a sample of gitea's ordinary use and the
requests named below. It reports each request the Core Rule Set refuses, so that requests named below. It reports each request the Core Rule Set refuses, so that
these notes can be checked again when gitea or the Core Rule Set changes these notes can be checked again when gitea or the Core Rule Set changes
version. In `detect` mode, as the setup runs it, the same requests are logged version.
with their rule ids, and pass.
Each refusal answers only that request ("Bans" in `SPEC.md` says what it counts ## What the defaults cover
toward). The request log names the rule in `waf_rule_ids`, which
`SWWAF_WAF_DISABLED_RULES` can switch off.
### What it lets through - SSH access to gitea does not pass through `smallwebwaf` and is not protected
by it.
- A clone is a response. It fits the defaults of 30 minutes and 1 GiB unless the
repository is large or the link slow. A repository whose clone is larger than
1 GiB needs `SWRP_MAX_RESPONSE_SIZE` raised, and the byte limits with it
("Byte limits, per client" in `SPEC.md`).
- A push is a request, and so is every other upload:
- a git push;
- an LFS object;
- a container image layer;
- a package file;
- a release attachment.
The Core Rule Set lets gitea's ordinary use through, apart from the refusals in At the defaults, an upload larger than 100 MiB or taking more than 60
the sections after this one: seconds is cut off ("`rproxy`'s limits" in `SPEC.md`). A gitea that takes
large uploads needs `SWRP_MAX_REQUEST_SIZE` and `SWRP_MAX_REQUEST_TIME_MS`
raised to fit.
- Archive download and blame or history pages are what scrapers hammer. The
request limits do most of the work there.
- An Actions runner that reaches gitea through `smallwebwaf` sends requests all
day. One older than version 0.4 (April 2026) asks for work every 2 seconds,
43,200 requests a day. While it runs a job, it reports the job's log and state
every second instead, two requests a second. So one runner busy all day sends
120 requests a minute and 172,800 a day, under the request limits. Two such
runners on one address, each busy all day, make 345,600 and pass the day limit
of 300,000. Three busy at once make 21,600 an hour and pass the hour limit of
20,000. Five busy at once make 600 a minute and pass the minute limit of 500.
Their address is then banned, their running jobs fail, and each repeat within
a day makes the ban longer. Newer runners ask for work less often. Put the
runners' addresses in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
## What the Core Rule Set lets through
At the defaults, the Core Rule Set lets gitea's ordinary use through, apart from
the refusals in the next section:
- browsing and views of files in a repository, with their history, blame and the - browsing and views of files in a repository, with their history, blame and the
file tree; file tree;
@@ -193,6 +60,12 @@ the sections after this one:
editor, code included, since no body is read; editor, code included, since no body is read;
- the page gitea shows after a change, with its message naming what was done. - the page gitea shows after a change, with its message naming what was done.
## What the Core Rule Set still refuses
Each refusal answers only that request ("Bans" in `SPEC.md` says what it counts
toward). The request log names the rule in `waf_rule_ids`, which
`SWWAF_WAF_DISABLED_RULES` can switch off.
### A query string that reads as an attack ### A query string that reads as an attack
Most often this is a search. A search term is a query parameter, which the Core Most often this is a search. A search term is a query parameter, which the Core
@@ -310,7 +183,7 @@ the runner. The error burst ("Attack detection" in `SPEC.md`) bans it after more
than 30 such refusals from its address in a minute, as when many jobs upload at than 30 such refusals from its address in a minute, as when many jobs upload at
once. `actions/upload-artifact@v4` does not send the header. once. `actions/upload-artifact@v4` does not send the header.
### What body inspection refuses ## What body inspection refuses
With `SWWAF_WAF_BODY_LIMIT` set, the Core Rule Set reads request bodies too. In With `SWWAF_WAF_BODY_LIMIT` set, the Core Rule Set reads request bodies too. In
front of gitea it then refuses these, until their rule ids, named in the request front of gitea it then refuses these, until their rule ids, named in the request
@@ -322,7 +195,7 @@ log, are added to `SWWAF_WAF_DISABLED_RULES`:
- PyPI uploads (922130); - PyPI uploads (922130);
- attachments named like `debug.log` or `config.yml` (932180). - attachments named like `debug.log` or `config.yml` (932180).
### What `script/gitea-requests` sends ## What `script/gitea-requests` sends
Of the requests above, the script sends: Of the requests above, the script sends:
+61 -61
View File
@@ -16,9 +16,9 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rproxy" "sneak.berlin/go/smallwebwaf/internal/rproxy"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// post is the start of a POST whose headers end with what follows it. // post is the start of a POST whose headers end with what follows it.
@@ -71,11 +71,11 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceServeWAFAllowsIt(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked") t.Error("the app was asked")
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestSize: tc.limit, maxRequestSize: tc.limit,
}) })
@@ -85,11 +85,11 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceServeWAFAllowsIt(t *testing.T) {
t.Errorf("answered %+v, want 413", got) t.Errorf("answered %+v, want 413", got)
} }
if asked := servewaf.question(t); len(asked.body) != tc.asked { if asked := standIn.question(t); len(asked.body) != tc.asked {
t.Errorf("the question carries %d bytes, want %d", len(asked.body), tc.asked) t.Errorf("the question carries %d bytes, want %d", len(asked.body), tc.asked)
} }
report := servewaf.report(t) report := standIn.report(t)
if report.Status != http.StatusRequestEntityTooLarge { if report.Status != http.StatusRequestEntityTooLarge {
t.Errorf("report %+v, want status 413", report) t.Errorf("report %+v, want status 413", report)
} }
@@ -100,9 +100,9 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceServeWAFAllowsIt(t *testing.T) {
func TestADenyOutranksTheBodySizeLimit(t *testing.T) { func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestSize: "1K", maxRequestSize: "1K",
}) })
@@ -132,12 +132,12 @@ func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
var asked atomic.Bool var asked atomic.Bool
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, r *http.Request) { app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
asked.Store(true) asked.Store(true)
greet(w, r) greet(w, r)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestSize: tc.limit, maxRequestSize: tc.limit,
}) })
@@ -165,12 +165,12 @@ func TestAResponseOverItsLimit(t *testing.T) {
t.Run("announced, is refused", func(t *testing.T) { t.Run("announced, is refused", func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(contentLengthHeader, strconv.Itoa(len(large))) w.Header().Set(contentLengthHeader, strconv.Itoa(len(large)))
_, _ = io.WriteString(w, large) _, _ = io.WriteString(w, large)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxResponseSize: "1K", maxResponseSize: "1K",
}) })
@@ -179,7 +179,7 @@ func TestAResponseOverItsLimit(t *testing.T) {
t.Errorf("answered %d, want 502", got.status) t.Errorf("answered %d, want 502", got.status)
} }
if report := servewaf.report(t); report.Status != http.StatusBadGateway { if report := standIn.report(t); report.Status != http.StatusBadGateway {
t.Errorf("report %+v, want status 502", report) t.Errorf("report %+v, want status 502", report)
} }
}) })
@@ -187,13 +187,13 @@ func TestAResponseOverItsLimit(t *testing.T) {
t.Run("not announced, is cut off", func(t *testing.T) { t.Run("not announced, is cut off", func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, large[:1]) _, _ = io.WriteString(w, large[:1])
_ = http.NewResponseController(w).Flush() _ = http.NewResponseController(w).Flush()
_, _ = io.WriteString(w, large[1:]) _, _ = io.WriteString(w, large[1:])
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxResponseSize: "1K", maxResponseSize: "1K",
}) })
@@ -205,7 +205,7 @@ func TestAResponseOverItsLimit(t *testing.T) {
t.Errorf("read %d bytes (%v), want the answer cut off", len(text), err) t.Errorf("read %d bytes (%v), want the answer cut off", len(text), err)
} }
report := servewaf.report(t) report := standIn.report(t)
if report.Status != http.StatusOK || report.ResponseBytes > 1<<10 { if report.Status != http.StatusOK || report.ResponseBytes > 1<<10 {
t.Errorf("report %+v, want status 200 and at most 1K of the answer", report) t.Errorf("report %+v, want status 200 and at most 1K of the answer", report)
} }
@@ -217,11 +217,11 @@ func TestAHeadWhoseAnswerAnnouncesMoreThanTheResponseLimitIsPassedOn(t *testing.
const announced = "2048" const announced = "2048"
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(contentLengthHeader, announced) w.Header().Set(contentLengthHeader, announced)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxResponseSize: "1K", maxResponseSize: "1K",
}) })
@@ -240,9 +240,9 @@ func TestASettingOfOffSetsNoLimit(t *testing.T) {
t.Run(setting, func(t *testing.T) { t.Run(setting, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
setting: "off", setting: "off",
}) })
@@ -251,7 +251,7 @@ func TestASettingOfOffSetsNoLimit(t *testing.T) {
wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/", wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/",
strings.NewReader("a body")))) strings.NewReader("a body"))))
if got := get(t, addr, proxy.HealthPath); got.status != http.StatusOK { if got := get(t, addr, servewaf.HealthPath); got.status != http.StatusOK {
t.Errorf("answered %+v, want rproxy's own 200", got) t.Errorf("answered %+v, want rproxy's own 200", got)
} }
}) })
@@ -270,9 +270,9 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "500", maxRequestTime: "500",
}) })
@@ -288,11 +288,11 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
t.Errorf("line %v, want no disposition and 10 bytes", line) t.Errorf("line %v, want no disposition and 10 bytes", line)
} }
if len(servewaf.questions) != 0 { if len(standIn.questions) != 0 {
t.Error("servewaf was asked") t.Error("servewaf was asked")
} }
servewaf.wantNoReport(t) standIn.wantNoReport(t)
}) })
} }
} }
@@ -300,9 +300,9 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) { func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, post+announcedLength(100)+"some of it") send(t, conn, post+announcedLength(100)+"some of it")
@@ -313,7 +313,7 @@ func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
t.Errorf("line %v, want no answer and no disposition", line) t.Errorf("line %v, want no answer and no disposition", line)
} }
if len(servewaf.questions) != 0 { if len(standIn.questions) != 0 {
t.Error("servewaf was asked") t.Error("servewaf was asked")
} }
} }
@@ -334,9 +334,9 @@ func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, tc.request) send(t, conn, tc.request)
@@ -364,7 +364,7 @@ func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
t.Errorf("line %v, want no answer and no disposition", line) t.Errorf("line %v, want no answer and no disposition", line)
} }
if len(servewaf.questions) != 0 { if len(standIn.questions) != 0 {
t.Error("servewaf was asked") t.Error("servewaf was asked")
} }
}) })
@@ -404,11 +404,11 @@ func TestABodyLeftUnreadIsReadOnlyUntilTheRequestTimeLimit(t *testing.T) {
announced = 100 * questionBodyMaxBytes announced = 100 * questionBodyMaxBytes
) )
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
}) })
_, _, server := startServer(t, servewaf.URL, app.URL, map[string]string{ _, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: strconv.FormatInt(limit.Milliseconds(), 10), maxRequestTime: strconv.FormatInt(limit.Milliseconds(), 10),
}) })
@@ -454,9 +454,9 @@ func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
_, _, server := startServer(t, servewaf.URL, app.URL, map[string]string{ _, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10), maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10),
}) })
@@ -484,9 +484,9 @@ func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) { func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "3000", maxRequestTime: "3000",
}) })
@@ -498,11 +498,11 @@ func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
t.Errorf("status %d, want 408", status) t.Errorf("status %d, want 408", status)
} }
if asked := servewaf.question(t); len(asked.body) != questionBodyMaxBytes { if asked := standIn.question(t); len(asked.body) != questionBodyMaxBytes {
t.Errorf("the question carries %d bytes", len(asked.body)) t.Errorf("the question carries %d bytes", len(asked.body))
} }
if report := servewaf.report(t); report.Status != http.StatusRequestTimeout { if report := standIn.report(t); report.Status != http.StatusRequestTimeout {
t.Errorf("report %+v, want status 408", report) t.Errorf("report %+v, want status 408", report)
} }
} }
@@ -553,8 +553,8 @@ func TestAnAppTooSlowToTakeTheRequestIsAnswered504(t *testing.T) {
writeSize = 32 << 10 writeSize = 32 << 10
) )
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
addr, _ := startRProxy(t, servewaf.URL, startAppThatTakesNothing(t), addr, _ := startRProxy(t, standIn.URL, startAppThatTakesNothing(t),
map[string]string{maxRequestTime: "3000"}) map[string]string{maxRequestTime: "3000"})
conn := dial(t, addr) conn := dial(t, addr)
@@ -580,14 +580,14 @@ func TestAnAppTooSlowToAnswerIsAnswered504(t *testing.T) {
t.Parallel() t.Parallel()
release := make(chan struct{}) release := make(chan struct{})
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
<-release <-release
}) })
t.Cleanup(func() { close(release) }) t.Cleanup(func() { close(release) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxResponseTime: "300", maxResponseTime: "300",
}) })
@@ -596,7 +596,7 @@ func TestAnAppTooSlowToAnswerIsAnswered504(t *testing.T) {
t.Errorf("answered %d, want 504", got.status) t.Errorf("answered %d, want 504", got.status)
} }
if report := servewaf.report(t); report.Status != http.StatusGatewayTimeout { if report := standIn.report(t); report.Status != http.StatusGatewayTimeout {
t.Errorf("report %+v, want status 504", report) t.Errorf("report %+v, want status 504", report)
} }
} }
@@ -607,7 +607,7 @@ func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
const start = "the start" const start = "the start"
release := make(chan struct{}) release := make(chan struct{})
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, start) _, _ = io.WriteString(w, start)
_ = http.NewResponseController(w).Flush() _ = http.NewResponseController(w).Flush()
@@ -619,7 +619,7 @@ func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
// The time limit is long enough for a loaded host to pass on the start of the // The time limit is long enough for a loaded host to pass on the start of the
// answer before it runs out. // answer before it runs out.
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxResponseTime: "3000", maxResponseTime: "3000",
}) })
@@ -642,7 +642,7 @@ func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
res.StatusCode, body, err, start) res.StatusCode, body, err, start)
} }
if report := servewaf.report(t); report.Status != http.StatusOK { if report := standIn.report(t); report.Status != http.StatusOK {
t.Errorf("report %+v, want the status the visitor got", report) t.Errorf("report %+v, want the status the visitor got", report)
} }
} }
@@ -656,7 +656,7 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
writeSize = 32 << 10 writeSize = 32 << 10
) )
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
part := make([]byte, writeSize) part := make([]byte, writeSize)
@@ -669,7 +669,7 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
}) })
// The time limit is long enough for a loaded host to start passing on the // The time limit is long enough for a loaded host to start passing on the
// answer before it runs out. // answer before it runs out.
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxResponseTime: "3000", maxResponseTime: "3000",
}) })
@@ -684,7 +684,7 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
t.Errorf("line %v, want status 200 and the answer cut off", line) t.Errorf("line %v, want status 200 and the answer cut off", line)
} }
report := servewaf.report(t) report := standIn.report(t)
if report.Status != http.StatusOK || report.ResponseBytes >= bodySize { if report.Status != http.StatusOK || report.ResponseBytes >= bodySize {
t.Errorf("report %+v, want status 200 and the answer cut off", report) t.Errorf("report %+v, want status 200 and the answer cut off", report)
} }
@@ -693,15 +693,15 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) { func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
addr, _ := startRProxy(t, servewaf.URL, nowhere, nil) addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
got := get(t, addr, "/") got := get(t, addr, "/")
if got.status != http.StatusBadGateway { if got.status != http.StatusBadGateway {
t.Errorf("answered %d, want 502", got.status) t.Errorf("answered %d, want 502", got.status)
} }
if report := servewaf.report(t); report.Status != http.StatusBadGateway { if report := standIn.report(t); report.Status != http.StatusBadGateway {
t.Errorf("report %+v, want status 502", report) t.Errorf("report %+v, want status 502", report)
} }
} }
@@ -709,11 +709,11 @@ func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing.T) { func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, greeting) _, _ = io.WriteString(w, greeting)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "3000", maxRequestTime: "3000",
}) })
@@ -732,12 +732,12 @@ func TestAVisitorThatStopsSendingWhileTheAppWorksGetsNoAnswer(t *testing.T) {
t.Parallel() t.Parallel()
working := make(chan struct{}) working := make(chan struct{})
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) { app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
close(working) close(working)
<-r.Context().Done() <-r.Context().Done()
}) })
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n") send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n")
@@ -773,7 +773,7 @@ func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing
const answer = "the end of a long answer" const answer = "the end of a long answer"
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, r *http.Request) { app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(io.Discard, r.Body) _, _ = io.Copy(io.Discard, r.Body)
_ = http.NewResponseController(w).Flush() _ = http.NewResponseController(w).Flush()
@@ -782,7 +782,7 @@ func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing
_, _ = io.WriteString(w, answer) _, _ = io.WriteString(w, answer)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "2000", maxRequestTime: "2000",
}) })
@@ -802,18 +802,18 @@ func TestAVisitorThatStopsSendingGetsAnAnswerRProxyGivesByTheDeadline(t *testing
{"a deny", post, http.StatusForbidden}, {"a deny", post, http.StatusForbidden},
{ {
"the health check", "the health check",
"GET " + proxy.HealthPath + " HTTP/1.1\r\nHost: site.example\r\n", "GET " + servewaf.HealthPath + " HTTP/1.1\r\nHost: site.example\r\n",
http.StatusOK, http.StatusOK,
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
// The time limit is long enough for a loaded host to read the first part of // The time limit is long enough for a loaded host to read the first part of
// the body before it runs out. // the body before it runs out.
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "3000", maxRequestTime: "3000",
}) })
+51 -51
View File
@@ -12,11 +12,11 @@ import (
"strings" "strings"
"testing" "testing"
"sneak.berlin/go/smallwebwaf/internal/proxy" "sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// questionBodyMaxBytes is the most of a body a question carries. // questionBodyMaxBytes is the most of a body a question carries.
const questionBodyMaxBytes = proxy.QuestionBodyMaxBytes const questionBodyMaxBytes = servewaf.QuestionBodyMaxBytes
// chunked is the encoding of a body whose length the visitor did not announce. // chunked is the encoding of a body whose length the visitor did not announce.
const chunked = "chunked" const chunked = "chunked"
@@ -29,9 +29,9 @@ const pathGoReencodes = "/some%2Fpath/with%20space|pipe"
func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) { func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
const target = pathGoReencodes + "?b=2&a=1&bad=%zz;x" const target = pathGoReencodes + "?b=2&a=1&bad=%zz;x"
@@ -42,16 +42,16 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
req.Header.Set(forwardedForHeader, "198.51.100.1") req.Header.Set(forwardedForHeader, "198.51.100.1")
req.Header.Set(forwardedProtoHeader, "gopher") req.Header.Set(forwardedProtoHeader, "gopher")
req.Header.Set(requestIDHeader, "the-visitor's") req.Header.Set(requestIDHeader, "the-visitor's")
req.Header.Set(proxy.ClientHeader, "198.51.100.2") req.Header.Set(servewaf.ClientHeader, "198.51.100.2")
req.Header.Set(proxy.PeerHeader, "198.51.100.3") req.Header.Set(servewaf.PeerHeader, "198.51.100.3")
req.Header.Set(proxy.ContentLengthHeader, "12345") req.Header.Set(servewaf.ContentLengthHeader, "12345")
// Go's client then sends no User-Agent, so that one in the question would be // Go's client then sends no User-Agent, so that one in the question would be
// Go's own. // Go's own.
req.Header["User-Agent"] = nil req.Header["User-Agent"] = nil
wantGreeting(t, visit(t, req)) wantGreeting(t, visit(t, req))
asked := servewaf.question(t) asked := standIn.question(t)
if asked.method != http.MethodDelete || asked.target != target || if asked.method != http.MethodDelete || asked.target != target ||
asked.host != site { asked.host != site {
t.Errorf("asked about %s %s at %s", asked.method, asked.target, asked.host) t.Errorf("asked about %s %s at %s", asked.method, asked.target, asked.host)
@@ -68,8 +68,8 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
want.Set(forwardedForHeader, "198.51.100.1") want.Set(forwardedForHeader, "198.51.100.1")
want.Set(forwardedProtoHeader, "http") want.Set(forwardedProtoHeader, "http")
want.Set(requestIDHeader, asked.header.Get(requestIDHeader)) want.Set(requestIDHeader, asked.header.Get(requestIDHeader))
want.Set(proxy.ClientHeader, localhost) want.Set(servewaf.ClientHeader, localhost)
want.Set(proxy.PeerHeader, localhost) want.Set(servewaf.PeerHeader, localhost)
if !maps.EqualFunc(asked.header, want, slices.Equal) { if !maps.EqualFunc(asked.header, want, slices.Equal) {
t.Errorf("the question's headers are %v, want %v", asked.header, want) t.Errorf("the question's headers are %v, want %v", asked.header, want)
@@ -93,14 +93,14 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
forwarded := make(chan http.Header, 1) forwarded := make(chan http.Header, 1)
app := startApp(t, func(w http.ResponseWriter, r *http.Request) { app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
forwarded <- r.Header.Clone() forwarded <- r.Header.Clone()
greet(w, r) greet(w, r)
}) })
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: tc.trusted, trustedProxies: tc.trusted,
}) })
@@ -109,9 +109,9 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
req.Header.Set(forwardedProtoHeader, "https") req.Header.Set(forwardedProtoHeader, "https")
wantGreeting(t, visit(t, req)) wantGreeting(t, visit(t, req))
asked := servewaf.question(t) asked := standIn.question(t)
if asked.header.Get(proxy.ClientHeader) != tc.client || if asked.header.Get(servewaf.ClientHeader) != tc.client ||
asked.header.Get(proxy.PeerHeader) != localhost || asked.header.Get(servewaf.PeerHeader) != localhost ||
asked.header.Get(forwardedForHeader) != visitor || asked.header.Get(forwardedForHeader) != visitor ||
asked.header.Get(forwardedProtoHeader) != tc.proto { asked.header.Get(forwardedProtoHeader) != tc.proto {
t.Errorf("the question's headers are %v", asked.header) t.Errorf("the question's headers are %v", asked.header)
@@ -157,9 +157,9 @@ func TestQuestionNamesTheClientAsTheForwardedAddressesSay(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: localhost + "/32,10.0.0.0/8", trustedProxies: localhost + "/32,10.0.0.0/8",
}) })
@@ -167,7 +167,7 @@ func TestQuestionNamesTheClientAsTheForwardedAddressesSay(t *testing.T) {
req.Header[forwardedForHeader] = tc.header req.Header[forwardedForHeader] = tc.header
wantGreeting(t, visit(t, req)) wantGreeting(t, visit(t, req))
if got := servewaf.question(t).header.Get(proxy.ClientHeader); got != tc.client { if got := standIn.question(t).header.Get(servewaf.ClientHeader); got != tc.client {
t.Errorf("the question names %s, want %s", got, tc.client) t.Errorf("the question names %s, want %s", got, tc.client)
} }
}) })
@@ -197,7 +197,7 @@ func TestFirst64KiBReachServeWAFAndTheWholeBodyTheApp(t *testing.T) {
t.Parallel() t.Parallel()
body := randomBytes(size) body := randomBytes(size)
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
received := make(chan []byte, 1) received := make(chan []byte, 1)
app := startApp(t, func(w http.ResponseWriter, r *http.Request) { app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
whole, _ := io.ReadAll(r.Body) whole, _ := io.ReadAll(r.Body)
@@ -205,7 +205,7 @@ func TestFirst64KiBReachServeWAFAndTheWholeBodyTheApp(t *testing.T) {
greet(w, r) greet(w, r)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
req := newRequest(t, http.MethodPost, addr, "/", bytes.NewReader(body)) req := newRequest(t, http.MethodPost, addr, "/", bytes.NewReader(body))
if !announced { if !announced {
@@ -215,7 +215,7 @@ func TestFirst64KiBReachServeWAFAndTheWholeBodyTheApp(t *testing.T) {
wantGreeting(t, visit(t, req)) wantGreeting(t, visit(t, req))
asked := servewaf.question(t) asked := standIn.question(t)
want := body[:min(size, questionBodyMaxBytes)] want := body[:min(size, questionBodyMaxBytes)]
wantQuestionBody(t, asked, want, size, announced) wantQuestionBody(t, asked, want, size, announced)
@@ -223,7 +223,7 @@ func TestFirst64KiBReachServeWAFAndTheWholeBodyTheApp(t *testing.T) {
t.Error("the app did not get the whole body") t.Error("the app did not get the whole body")
} }
if report := servewaf.report(t); report.RequestBytes != int64(size) { if report := standIn.report(t); report.RequestBytes != int64(size) {
t.Errorf("report %+v, of %d bytes", report, size) t.Errorf("report %+v, of %d bytes", report, size)
} }
}) })
@@ -243,7 +243,7 @@ func wantQuestionBody(
len(want)) len(want))
} }
values := asked.header.Values(proxy.ContentLengthHeader) values := asked.header.Values(servewaf.ContentLengthHeader)
if announced { if announced {
if !slices.Equal(values, []string{strconv.Itoa(size)}) || if !slices.Equal(values, []string{strconv.Itoa(size)}) ||
@@ -264,9 +264,9 @@ func wantQuestionBody(
func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) { func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
// Far more announced than sent. Go's server does not wait for that much, so // Far more announced than sent. Go's server does not wait for that much, so
// the answer comes at once. // the answer comes at once.
@@ -278,7 +278,7 @@ func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) {
t.Errorf("status %d, want servewaf's 403", status) t.Errorf("status %d, want servewaf's 403", status)
} }
if report := servewaf.report(t); report.RequestBytes != questionBodyMaxBytes { if report := standIn.report(t); report.RequestBytes != questionBodyMaxBytes {
t.Errorf("report %+v, want the first part's bytes", report) t.Errorf("report %+v, want the first part's bytes", report)
} }
} }
@@ -306,8 +306,8 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) {
t.Run(method+" "+tc.name, func(t *testing.T) { t.Run(method+" "+tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
addr, _ := startRProxy(t, servewaf.URL, nowhere, nil) addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, method+" /x HTTP/1.1\r\nHost: site.example\r\n"+tc.framing) send(t, conn, method+" /x HTTP/1.1\r\nHost: site.example\r\n"+tc.framing)
@@ -316,7 +316,7 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) {
t.Errorf("status %d, want servewaf's 403", status) t.Errorf("status %d, want servewaf's 403", status)
} }
asked := servewaf.question(t) asked := standIn.question(t)
if string(asked.body) != tc.body || if string(asked.body) != tc.body ||
!slices.Equal(asked.transferEncoding, tc.encoding) { !slices.Equal(asked.transferEncoding, tc.encoding) {
t.Errorf("the question carries %q, encoded %v, want %q, encoded %v", t.Errorf("the question carries %q, encoded %v, want %q, encoded %v",
@@ -334,11 +334,11 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
t.Run(method, func(t *testing.T) { t.Run(method, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked") t.Error("the app was asked")
}) })
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
got := visit(t, newRequest(t, method, addr, "/", http.NoBody)) got := visit(t, newRequest(t, method, addr, "/", http.NoBody))
if got.status != http.StatusMethodNotAllowed || if got.status != http.StatusMethodNotAllowed ||
@@ -352,11 +352,11 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
t.Errorf("line %v, want status 405 and no disposition", line) t.Errorf("line %v, want status 405 and no disposition", line)
} }
if len(servewaf.questions) != 0 { if len(standIn.questions) != 0 {
t.Error("servewaf was asked") t.Error("servewaf was asked")
} }
servewaf.wantNoReport(t) standIn.wantNoReport(t)
}) })
} }
} }
@@ -372,18 +372,18 @@ func TestEachMethodPassedOnReachesServeWAFAndTheApp(t *testing.T) {
t.Parallel() t.Parallel()
reached := make(chan string, 1) reached := make(chan string, 1)
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) { app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
reached <- r.Method reached <- r.Method
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
got := visit(t, newRequest(t, method, addr, "/", http.NoBody)) got := visit(t, newRequest(t, method, addr, "/", http.NoBody))
if got.status != http.StatusOK { if got.status != http.StatusOK {
t.Errorf("answered %+v, want the app's 200", got) t.Errorf("answered %+v, want the app's 200", got)
} }
if asked := servewaf.question(t); asked.method != method { if asked := standIn.question(t); asked.method != method {
t.Errorf("servewaf was asked about %s", asked.method) t.Errorf("servewaf was asked about %s", asked.method)
} }
@@ -416,9 +416,9 @@ func TestARequestThatCannotGoToServeWAFAsSentIsRefused(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, request) send(t, conn, request)
@@ -433,11 +433,11 @@ func TestARequestThatCannotGoToServeWAFAsSentIsRefused(t *testing.T) {
t.Errorf("line %v, want status 400 and no disposition", line) t.Errorf("line %v, want status 400 and no disposition", line)
} }
if len(servewaf.questions) != 0 { if len(standIn.questions) != 0 {
t.Error("servewaf was asked") t.Error("servewaf was asked")
} }
servewaf.wantNoReport(t) standIn.wantNoReport(t)
}) })
} }
} }
@@ -447,9 +447,9 @@ func TestAHostWithAPercentSignButNoBracketGoesToServeWAF(t *testing.T) {
const host = "a%b" const host = "a%b"
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, "GET / HTTP/1.1\r\nHost: "+host+"\r\n\r\n") send(t, conn, "GET / HTTP/1.1\r\nHost: "+host+"\r\n\r\n")
@@ -458,7 +458,7 @@ func TestAHostWithAPercentSignButNoBracketGoesToServeWAF(t *testing.T) {
t.Errorf("status %d, want the app's 200", status) t.Errorf("status %d, want the app's 200", status)
} }
if asked := servewaf.question(t); asked.host != host { if asked := standIn.question(t); asked.host != host {
t.Errorf("asked about the host %q, want %q", asked.host, host) t.Errorf("asked about the host %q, want %q", asked.host, host)
} }
} }
@@ -468,15 +468,15 @@ func TestATrustedPeerWithNoForwardedProtoIsAskedAboutAsHTTP(t *testing.T) {
const want = "http" const want = "http"
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: localhost + "/32", trustedProxies: localhost + "/32",
}) })
wantGreeting(t, get(t, addr, "/")) wantGreeting(t, get(t, addr, "/"))
asked := servewaf.question(t) asked := standIn.question(t)
if got := asked.header.Get(forwardedProtoHeader); got != want { if got := asked.header.Get(forwardedProtoHeader); got != want {
t.Errorf("the question's %s is %q, want %s", forwardedProtoHeader, got, want) t.Errorf("the question's %s is %q, want %s", forwardedProtoHeader, got, want)
} }
@@ -497,9 +497,9 @@ func TestAQuestionNamesNoZone(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
_, out, server := startServer(t, servewaf.URL, app.URL, map[string]string{ _, out, server := startServer(t, standIn.URL, app.URL, map[string]string{
trustedProxies: localhost + "/32", trustedProxies: localhost + "/32",
}) })
@@ -509,9 +509,9 @@ func TestAQuestionNamesNoZone(t *testing.T) {
server.Handler.ServeHTTP(httptest.NewRecorder(), req) server.Handler.ServeHTTP(httptest.NewRecorder(), req)
asked := servewaf.question(t) asked := standIn.question(t)
if asked.header.Get(proxy.ClientHeader) != tc.client || if asked.header.Get(servewaf.ClientHeader) != tc.client ||
asked.header.Get(proxy.PeerHeader) != tc.peer { asked.header.Get(servewaf.PeerHeader) != tc.peer {
t.Errorf("the question's headers are %v", asked.header) t.Errorf("the question's headers are %v", asked.header)
} }
+3 -3
View File
@@ -16,9 +16,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rules" "sneak.berlin/go/smallwebwaf/internal/rules"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// flushAfterEachWrite has ReverseProxy pass on each part of the app's answer as // flushAfterEachWrite has ReverseProxy pass on each part of the app's answer as
@@ -129,7 +129,7 @@ func (rq *request) readFirstPartOfBody() bool {
} }
_ = rq.rc.SetReadDeadline(rq.requestDeadline()) _ = rq.rc.SetReadDeadline(rq.requestDeadline())
held, err := io.ReadAll(io.LimitReader(rq.body, proxy.QuestionBodyMaxBytes)) held, err := io.ReadAll(io.LimitReader(rq.body, servewaf.QuestionBodyMaxBytes))
// From here on, the request timer ends a read of the rest of the body, so that // From here on, the request timer ends a read of the rest of the body, so that
// the answer can name the side rproxy was waiting on. // the answer can name the side rproxy was waiting on.
_ = rq.rc.SetReadDeadline(time.Time{}) _ = rq.rc.SetReadDeadline(time.Time{})
@@ -384,7 +384,7 @@ func (rq *request) finish(ctx context.Context) {
return return
} }
report := &proxy.Report{ report := &servewaf.Report{
RequestID: rq.id, RequestID: rq.id,
Status: min(rq.out.status, reportStatusMax), Status: min(rq.out.status, reportStatusMax),
RequestBytes: requestBytes, RequestBytes: requestBytes,
+5 -5
View File
@@ -22,7 +22,7 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/proxy" "sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// How rproxy keeps connections to the app and to servewaf open between // How rproxy keeps connections to the app and to servewaf open between
@@ -85,7 +85,7 @@ func New(params Params) *Server {
transport: newTransport(), transport: newTransport(),
serveWAFTransport: newServeWAFTransport(), serveWAFTransport: newServeWAFTransport(),
reportURL: (&url.URL{ reportURL: (&url.URL{
Scheme: backend.Scheme, Host: backend.Host, Path: proxy.ReportPath, Scheme: backend.Scheme, Host: backend.Host, Path: servewaf.ReportPath,
}).String(), }).String(),
} }
@@ -166,7 +166,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rq := h.newRequest(w, r) rq := h.newRequest(w, r)
defer rq.finish(r.Context()) defer rq.finish(r.Context())
if r.Method == http.MethodGet && r.URL.Path == proxy.HealthPath { if r.Method == http.MethodGet && r.URL.Path == servewaf.HealthPath {
rq.answerHealth() rq.answerHealth()
return return
@@ -199,9 +199,9 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rq.noteAnswer(answer) rq.noteAnswer(answer)
switch rq.disposition { switch rq.disposition {
case proxy.DispositionAllow: case servewaf.DispositionAllow:
rq.passToApp(r.Context()) rq.passToApp(r.Context())
case proxy.DispositionClose: case servewaf.DispositionClose:
// This panic has Go's server close the connection without an answer, // This panic has Go's server close the connection without an answer,
// and log nothing; the deferred finish still runs. // and log nothing; the deferred finish still runs.
panic(http.ErrAbortHandler) panic(http.ErrAbortHandler)
+80 -80
View File
@@ -19,9 +19,9 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rproxy" "sneak.berlin/go/smallwebwaf/internal/rproxy"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -54,7 +54,7 @@ const (
// The headers the tests set and look at. // The headers the tests set and look at.
const ( const (
dispositionHeader = proxy.DispositionHeader dispositionHeader = servewaf.DispositionHeader
forwardedForHeader = "X-Forwarded-For" forwardedForHeader = "X-Forwarded-For"
forwardedProtoHeader = "X-Forwarded-Proto" forwardedProtoHeader = "X-Forwarded-Proto"
requestIDHeader = "X-Request-ID" requestIDHeader = "X-Request-ID"
@@ -70,9 +70,9 @@ const (
// What the stand-in for servewaf answers. // What the stand-in for servewaf answers.
const ( const (
dispositionAllow = proxy.DispositionAllow dispositionAllow = servewaf.DispositionAllow
dispositionDeny = proxy.DispositionDeny dispositionDeny = servewaf.DispositionDeny
dispositionClose = proxy.DispositionClose dispositionClose = servewaf.DispositionClose
// deniedBody is the body of a deny. // deniedBody is the body of a deny.
deniedBody = "refused by servewaf" deniedBody = "refused by servewaf"
) )
@@ -173,7 +173,7 @@ type serveWAF struct {
*httptest.Server *httptest.Server
questions chan question questions chan question
reports chan proxy.Report reports chan servewaf.Report
} }
// startServeWAF starts a stand-in for servewaf. // startServeWAF starts a stand-in for servewaf.
@@ -182,11 +182,11 @@ func startServeWAF(t *testing.T, answer http.HandlerFunc) *serveWAF {
s := &serveWAF{ s := &serveWAF{
questions: make(chan question, 10), questions: make(chan question, 10),
reports: make(chan proxy.Report, 10), reports: make(chan servewaf.Report, 10),
} }
s.Server = httptest.NewServer(http.HandlerFunc( s.Server = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(proxy.ClientHeader) == "" { if r.Header.Get(servewaf.ClientHeader) == "" {
s.takeReport(t, w, r) s.takeReport(t, w, r)
return return
@@ -213,10 +213,10 @@ func startServeWAF(t *testing.T, answer http.HandlerFunc) *serveWAF {
func (s *serveWAF) takeReport(t *testing.T, w http.ResponseWriter, r *http.Request) { func (s *serveWAF) takeReport(t *testing.T, w http.ResponseWriter, r *http.Request) {
t.Helper() t.Helper()
var report proxy.Report var report servewaf.Report
err := json.NewDecoder(r.Body).Decode(&report) err := json.NewDecoder(r.Body).Decode(&report)
if err != nil || r.Method != http.MethodPost || r.URL.Path != proxy.ReportPath { if err != nil || r.Method != http.MethodPost || r.URL.Path != servewaf.ReportPath {
t.Errorf("%s %s is no report: %v", r.Method, r.URL.Path, err) t.Errorf("%s %s is no report: %v", r.Method, r.URL.Path, err)
} }
@@ -237,7 +237,7 @@ func (s *serveWAF) wantNoReport(t *testing.T) {
} }
// report waits for a report. // report waits for a report.
func (s *serveWAF) report(t *testing.T) proxy.Report { func (s *serveWAF) report(t *testing.T) servewaf.Report {
t.Helper() t.Helper()
select { select {
@@ -246,7 +246,7 @@ func (s *serveWAF) report(t *testing.T) proxy.Report {
case <-time.After(waitLimit): case <-time.After(waitLimit):
t.Fatal("no report") t.Fatal("no report")
return proxy.Report{} return servewaf.Report{}
} }
} }
@@ -273,7 +273,7 @@ func startServeWAFAnsweringReports(
backend := httptest.NewServer(http.HandlerFunc( backend := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(proxy.ClientHeader) != "" { if r.Header.Get(servewaf.ClientHeader) != "" {
answer(w, r) answer(w, r)
return return
@@ -498,7 +498,7 @@ func readStatus(t *testing.T, conn net.Conn) int {
func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) { func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, func(w http.ResponseWriter, r *http.Request) { standIn := startServeWAF(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(asnHeader, "AS64496") w.Header().Set(asnHeader, "AS64496")
w.Header().Set(countryHeader, "DE") w.Header().Set(countryHeader, "DE")
allow(w, r) allow(w, r)
@@ -512,7 +512,7 @@ func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) {
w.WriteHeader(http.StatusTeapot) w.WriteHeader(http.StatusTeapot)
_, _ = io.WriteString(w, greeting) _, _ = io.WriteString(w, greeting)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
const target = pathGoReencodes + "?b=2&a=1&bad=%zz;x" const target = pathGoReencodes + "?b=2&a=1&bad=%zz;x"
@@ -530,7 +530,7 @@ func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) {
} }
received := <-seen received := <-seen
asked := servewaf.question(t) asked := standIn.question(t)
if received.RequestURI != target || received.Host != site { if received.RequestURI != target || received.Host != site {
t.Errorf("the app saw %s for %s", received.RequestURI, received.Host) t.Errorf("the app saw %s for %s", received.RequestURI, received.Host)
@@ -557,14 +557,14 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip", "X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip",
} }
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
seen := make(chan http.Header, 1) seen := make(chan http.Header, 1)
app := startApp(t, func(w http.ResponseWriter, r *http.Request) { app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
seen <- r.Header.Clone() seen <- r.Header.Clone()
greet(w, r) greet(w, r)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: "10.0.0.0/8", trustedProxies: "10.0.0.0/8",
}) })
@@ -602,14 +602,14 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) { func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
seen := make(chan http.Header, 1) seen := make(chan http.Header, 1)
app := startApp(t, func(w http.ResponseWriter, r *http.Request) { app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
seen <- r.Header.Clone() seen <- r.Header.Clone()
greet(w, r) greet(w, r)
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: localhost + "/32", trustedProxies: localhost + "/32",
}) })
@@ -641,12 +641,12 @@ func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) { func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
w.Header()["Content-Type"] = nil w.Header()["Content-Type"] = nil
_, _ = io.WriteString(w, "<html>") _, _ = io.WriteString(w, "<html>")
}) })
_, _, server := startServer(t, servewaf.URL, app.URL, nil) _, _, server := startServer(t, standIn.URL, app.URL, nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody) req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@@ -704,11 +704,11 @@ func TestRemovesTheVisitorsLookupHeadersWhenServeWAFGivesNone(t *testing.T) {
func TestDenyGivesTheVisitorServeWAFsAnswer(t *testing.T) { func TestDenyGivesTheVisitorServeWAFsAnswer(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked") t.Error("the app was asked")
}) })
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
got := get(t, addr, "/") got := get(t, addr, "/")
if got.status != http.StatusForbidden || got.body != deniedBody || if got.status != http.StatusForbidden || got.body != deniedBody ||
@@ -720,7 +720,7 @@ func TestDenyGivesTheVisitorServeWAFsAnswer(t *testing.T) {
t.Errorf("the visitor got %s", dispositionHeader) t.Errorf("the visitor got %s", dispositionHeader)
} }
report := servewaf.report(t) report := standIn.report(t)
if report.Status != http.StatusForbidden || if report.Status != http.StatusForbidden ||
report.ResponseBytes != int64(len(deniedBody)) || report.ResponseBytes != int64(len(deniedBody)) ||
report.DurationUpstreamTotal != 0 { report.DurationUpstreamTotal != 0 {
@@ -736,9 +736,9 @@ func TestDenyGivesTheVisitorServeWAFsAnswer(t *testing.T) {
func TestADeniedHeadKeepsServeWAFsContentLength(t *testing.T) { func TestADeniedHeadKeepsServeWAFsContentLength(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
got := visit(t, newRequest(t, http.MethodHead, addr, "/", http.NoBody)) got := visit(t, newRequest(t, http.MethodHead, addr, "/", http.NoBody))
if got.status != http.StatusForbidden || if got.status != http.StatusForbidden ||
@@ -758,15 +758,15 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
}{ }{
{"a deny", "GET /" + host, deniedBody, http.StatusForbidden}, {"a deny", "GET /" + host, deniedBody, http.StatusForbidden},
{"rproxy's own answer", "GET x:admin" + host, "Bad Request\n", http.StatusBadRequest}, {"rproxy's own answer", "GET x:admin" + host, "Bad Request\n", http.StatusBadRequest},
{"the health check", "GET " + proxy.HealthPath + host, "ok\n", http.StatusOK}, {"the health check", "GET " + servewaf.HealthPath + host, "ok\n", http.StatusOK},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
log := heldLog{release: make(chan struct{})} log := heldLog{release: make(chan struct{})}
servewaf := startServeWAF(t, deny) standIn := startServeWAF(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startServerLogging(t, servewaf.URL, app.URL, nil, log, io.Discard) addr, _ := startServerLogging(t, standIn.URL, app.URL, nil, log, io.Discard)
t.Cleanup(func() { close(log.release) }) t.Cleanup(func() { close(log.release) })
@@ -794,9 +794,9 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
func TestRProxysOwnAnswerIsPlainText(t *testing.T) { func TestRProxysOwnAnswerIsPlainText(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, "GET x:admin HTTP/1.1\r\nHost: site.example\r\n\r\n") send(t, conn, "GET x:admin HTTP/1.1\r\nHost: site.example\r\n\r\n")
@@ -817,14 +817,14 @@ func TestRProxysOwnAnswerIsPlainText(t *testing.T) {
func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) { func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) { standIn := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(dispositionHeader, dispositionClose) w.Header().Set(dispositionHeader, dispositionClose)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
}) })
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked") t.Error("the app was asked")
}) })
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n") send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n")
@@ -835,7 +835,7 @@ func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
text, err) text, err)
} }
report := servewaf.report(t) report := standIn.report(t)
if report.Status != 0 || report.ResponseBytes != 0 { if report.Status != 0 || report.ResponseBytes != 0 {
t.Errorf("report %+v, of a visitor sent nothing", report) t.Errorf("report %+v, of a visitor sent nothing", report)
} }
@@ -935,7 +935,7 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsServeWAFGaveIt(t *testing.T) {
t.Run(tc.name+" with the default "+setting, func(t *testing.T) { t.Run(tc.name+" with the default "+setting, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) { standIn := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) {
if tc.disposition != "" { if tc.disposition != "" {
w.Header().Set(dispositionHeader, tc.disposition) w.Header().Set(dispositionHeader, tc.disposition)
} }
@@ -947,7 +947,7 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsServeWAFGaveIt(t *testing.T) {
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked") t.Error("the app was asked")
}) })
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
defaultDisposition: setting, defaultDisposition: setting,
}) })
@@ -977,11 +977,11 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsServeWAFGaveIt(t *testing.T) {
func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) { func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) { standIn := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "refused", http.StatusBadRequest) http.Error(w, "refused", http.StatusBadRequest)
}) })
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
if got := get(t, addr, "/"); got.status != http.StatusBadRequest { if got := get(t, addr, "/"); got.status != http.StatusBadRequest {
t.Errorf("answered %+v, want servewaf's 400", got) t.Errorf("answered %+v, want servewaf's 400", got)
@@ -991,18 +991,18 @@ func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) {
t.Errorf("line %v, want no disposition", line) t.Errorf("line %v, want no disposition", line)
} }
servewaf.wantNoReport(t) standIn.wantNoReport(t)
} }
func TestAnAnswerWithoutAContentTypeGoesToTheVisitorWithNone(t *testing.T) { func TestAnAnswerWithoutAContentTypeGoesToTheVisitorWithNone(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) { standIn := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) {
w.Header()["Content-Type"] = nil w.Header()["Content-Type"] = nil
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
_, _ = io.WriteString(w, "<html>broken") _, _ = io.WriteString(w, "<html>broken")
}) })
addr, _ := startRProxy(t, servewaf.URL, nowhere, nil) addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
got := get(t, addr, "/") got := get(t, addr, "/")
if contentType, sent := got.header["Content-Type"]; sent { if contentType, sent := got.header["Content-Type"]; sent {
@@ -1015,11 +1015,11 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
const size = 1 << 20 const size = 1 << 20
servewaf := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) { standIn := startServeWAF(t, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write(make([]byte, size)) _, _ = w.Write(make([]byte, size))
}) })
addr, _ := startRProxy(t, servewaf.URL, nowhere, nil) addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
got := get(t, addr, "/") got := get(t, addr, "/")
if got.status != http.StatusInternalServerError || len(got.body) != size { if got.status != http.StatusInternalServerError || len(got.body) != size {
@@ -1135,15 +1135,15 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) { func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, overOneMiB(dispositionDeny)) standIn := startServeWAF(t, overOneMiB(dispositionDeny))
addr, out := startRProxy(t, servewaf.URL, nowhere, nil) addr, out := startRProxy(t, standIn.URL, nowhere, nil)
if got := get(t, addr, "/"); got.status != http.StatusBadGateway { if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
t.Errorf("answered %+v, want 502", got) t.Errorf("answered %+v, want 502", got)
} }
out.requestLine(t) out.requestLine(t)
servewaf.wantNoReport(t) standIn.wantNoReport(t)
} }
func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) { func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
@@ -1199,20 +1199,20 @@ func TestAVisitorThatGoesAwayWhileServeWAFIsAskedGetsNoDefault(t *testing.T) {
t.Run("with the default "+disposition, func(t *testing.T) { t.Run("with the default "+disposition, func(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, func(_ http.ResponseWriter, r *http.Request) { standIn := startServeWAF(t, func(_ http.ResponseWriter, r *http.Request) {
<-r.Context().Done() <-r.Context().Done()
}) })
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked") t.Error("the app was asked")
}) })
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
timeoutSetting: "60000", timeoutSetting: "60000",
defaultDisposition: disposition, defaultDisposition: disposition,
}) })
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n") send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n")
servewaf.question(t) standIn.question(t)
_ = conn.Close() _ = conn.Close()
@@ -1226,7 +1226,7 @@ func TestAVisitorThatGoesAwayWhileServeWAFIsAskedGetsNoDefault(t *testing.T) {
t.Error("rproxy warned that servewaf gave no answer") t.Error("rproxy warned that servewaf gave no answer")
} }
servewaf.wantNoReport(t) standIn.wantNoReport(t)
}) })
} }
} }
@@ -1244,17 +1244,17 @@ func TestRProxyWarnsWhenTheDefaultDecides(t *testing.T) {
func TestSendsNoReportWithoutAnAnswer(t *testing.T) { func TestSendsNoReportWithoutAnAnswer(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, func(_ http.ResponseWriter, r *http.Request) { standIn := startServeWAF(t, func(_ http.ResponseWriter, r *http.Request) {
<-r.Context().Done() <-r.Context().Done()
}) })
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
timeoutSetting: "200", timeoutSetting: "200",
}) })
wantGreeting(t, get(t, addr, "/")) wantGreeting(t, get(t, addr, "/"))
out.requestLine(t) out.requestLine(t)
servewaf.wantNoReport(t) standIn.wantNoReport(t)
} }
func TestAPathUnderOwnPrefixNeverReachesTheApp(t *testing.T) { func TestAPathUnderOwnPrefixNeverReachesTheApp(t *testing.T) {
@@ -1274,25 +1274,25 @@ func TestAPathUnderOwnPrefixNeverReachesTheApp(t *testing.T) {
func TestHealthCheckIsAnsweredWhileServeWAFIsDown(t *testing.T) { func TestHealthCheckIsAnsweredWhileServeWAFIsDown(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, nowhere, app.URL, nil) addr, out := startRProxy(t, nowhere, app.URL, nil)
got := get(t, addr, proxy.HealthPath) got := get(t, addr, servewaf.HealthPath)
if got.status != http.StatusOK || got.body != "ok\n" { if got.status != http.StatusOK || got.body != "ok\n" {
t.Errorf("answered %+v, want ok", got) t.Errorf("answered %+v, want ok", got)
} }
line := out.requestLine(t) line := out.requestLine(t)
if line["path"] != proxy.HealthPath || line["disposition"] != nil { if line["path"] != servewaf.HealthPath || line["disposition"] != nil {
t.Errorf("line %v, want one with no disposition", line) t.Errorf("line %v, want one with no disposition", line)
} }
// With servewaf up too, it is not asked. // With servewaf up too, it is not asked.
addr, _ = startRProxy(t, servewaf.URL, app.URL, nil) addr, _ = startRProxy(t, standIn.URL, app.URL, nil)
get(t, addr, proxy.HealthPath) get(t, addr, servewaf.HealthPath)
if len(servewaf.questions) != 0 { if len(standIn.questions) != 0 {
t.Error("servewaf was asked about the health check") t.Error("servewaf was asked about the health check")
} }
} }
@@ -1396,15 +1396,15 @@ func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
func TestAReportDueAfterWaitForReportsIsDropped(t *testing.T) { func TestAReportDueAfterWaitForReportsIsDropped(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out, server := startServer(t, servewaf.URL, app.URL, nil) addr, out, server := startServer(t, standIn.URL, app.URL, nil)
server.WaitForReports() server.WaitForReports()
wantGreeting(t, get(t, addr, "/")) wantGreeting(t, get(t, addr, "/"))
out.requestLine(t) out.requestLine(t)
servewaf.wantNoReport(t) standIn.wantNoReport(t)
} }
func TestReportGivesWhatHappened(t *testing.T) { func TestReportGivesWhatHappened(t *testing.T) {
@@ -1412,14 +1412,14 @@ func TestReportGivesWhatHappened(t *testing.T) {
const body = "the visitor's body" const body = "the visitor's body"
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/", wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/",
strings.NewReader(body)))) strings.NewReader(body))))
report := servewaf.report(t) report := standIn.report(t)
if report.RequestID == "" || report.Status != http.StatusOK || if report.RequestID == "" || report.Status != http.StatusOK ||
report.RequestBytes != int64(len(body)) || report.RequestBytes != int64(len(body)) ||
@@ -1442,7 +1442,7 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
release := make(chan struct{}) release := make(chan struct{})
backend := httptest.NewServer(http.HandlerFunc( backend := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(proxy.ClientHeader) != "" { if r.Header.Get(servewaf.ClientHeader) != "" {
allow(w, r) allow(w, r)
return return
@@ -1497,9 +1497,9 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) { func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) {
t.Parallel() t.Parallel()
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, echoAfterUpgrade) app := startApp(t, echoAfterUpgrade)
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "2000", maxRequestTime: "2000",
maxResponseTime: "2000", maxResponseTime: "2000",
}) })
@@ -1535,7 +1535,7 @@ func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) {
_ = conn.Close() _ = conn.Close()
report := servewaf.report(t) report := standIn.report(t)
if report.Status != http.StatusSwitchingProtocols || if report.Status != http.StatusSwitchingProtocols ||
report.RequestBytes != int64(len(message)) || report.RequestBytes != int64(len(message)) ||
report.ResponseBytes != int64(len(message)) { report.ResponseBytes != int64(len(message)) {
@@ -1552,7 +1552,7 @@ func TestAWebSocketPassesOnThatTheVisitorStoppedSending(t *testing.T) {
const goodbye = "sent after the visitor stopped sending\n" const goodbye = "sent after the visitor stopped sending\n"
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
conn, buffered, err := http.NewResponseController(w).Hijack() conn, buffered, err := http.NewResponseController(w).Hijack()
if err != nil { if err != nil {
@@ -1572,7 +1572,7 @@ func TestAWebSocketPassesOnThatTheVisitorStoppedSending(t *testing.T) {
_, _ = buffered.WriteString(goodbye) _, _ = buffered.WriteString(goodbye)
_ = buffered.Flush() _ = buffered.Flush()
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, "GET /socket HTTP/1.1\r\nHost: app\r\n"+ send(t, conn, "GET /socket HTTP/1.1\r\nHost: app\r\n"+
@@ -1610,7 +1610,7 @@ func TestAWebSocketPassesOnThatTheVisitorStoppedSending(t *testing.T) {
//nolint:paralleltest // see above //nolint:paralleltest // see above
func TestASwitchNotPassedOnClosesTheAppsConnection(t *testing.T) { func TestASwitchNotPassedOnClosesTheAppsConnection(t *testing.T) {
closed := make(chan error, 1) closed := make(chan error, 1)
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
conn, buffered, err := http.NewResponseController(w).Hijack() conn, buffered, err := http.NewResponseController(w).Hijack()
if err != nil { if err != nil {
@@ -1633,7 +1633,7 @@ func TestASwitchNotPassedOnClosesTheAppsConnection(t *testing.T) {
_, err = buffered.ReadByte() _, err = buffered.ReadByte()
closed <- err closed <- err
}) })
addr, _ := startRProxy(t, servewaf.URL, app.URL, nil) addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, "GET /socket HTTP/1.1\r\nHost: app\r\n"+ send(t, conn, "GET /socket HTTP/1.1\r\nHost: app\r\n"+
@@ -1681,9 +1681,9 @@ func TestEachRequestGetsAFreshID(t *testing.T) {
const fromTraefik = "an-id-from-traefik" const fromTraefik = "an-id-from-traefik"
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: localhost + "/32", trustedProxies: localhost + "/32",
}) })
@@ -1694,7 +1694,7 @@ func TestEachRequestGetsAFreshID(t *testing.T) {
req.Header.Set(requestIDHeader, fromTraefik) req.Header.Set(requestIDHeader, fromTraefik)
wantGreeting(t, visit(t, req)) wantGreeting(t, visit(t, req))
ids = append(ids, servewaf.question(t).header.Get(requestIDHeader)) ids = append(ids, standIn.question(t).header.Get(requestIDHeader))
} }
if slices.Contains(ids, fromTraefik) || ids[0] == "" || ids[0] == ids[1] { if slices.Contains(ids, fromTraefik) || ids[0] == "" || ids[0] == ids[1] {
@@ -1711,16 +1711,16 @@ func TestLogLineGivesTheRequestAsSent(t *testing.T) {
body = "the visitor's body" body = "the visitor's body"
) )
servewaf := startServeWAF(t, allow) standIn := startServeWAF(t, allow)
app := startApp(t, greet) app := startApp(t, greet)
addr, out := startRProxy(t, servewaf.URL, app.URL, nil) addr, out := startRProxy(t, standIn.URL, app.URL, nil)
req := newRequest(t, http.MethodPost, addr, path+"?"+query, strings.NewReader(body)) req := newRequest(t, http.MethodPost, addr, path+"?"+query, strings.NewReader(body))
req.Host = site req.Host = site
wantGreeting(t, visit(t, req)) wantGreeting(t, visit(t, req))
line := out.requestLine(t) line := out.requestLine(t)
asked := servewaf.question(t) asked := standIn.question(t)
for name, want := range map[string]any{ for name, want := range map[string]any{
"type": "request", "type": "request",
+11 -11
View File
@@ -11,7 +11,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"sneak.berlin/go/smallwebwaf/internal/proxy" "sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// answerBodyMaxBytes is the most of the body of servewaf's answer that is read. // answerBodyMaxBytes is the most of the body of servewaf's answer that is read.
@@ -38,10 +38,10 @@ type servewafAnswer struct {
// notes nothing, and no report is due for it: servewaf holds no decision to report // notes nothing, and no report is due for it: servewaf holds no decision to report
// on. // on.
func (rq *request) noteAnswer(answer *servewafAnswer) { func (rq *request) noteAnswer(answer *servewafAnswer) {
disposition := answer.header.Get(proxy.DispositionHeader) disposition := answer.header.Get(servewaf.DispositionHeader)
switch disposition { switch disposition {
case proxy.DispositionAllow, proxy.DispositionDeny, proxy.DispositionClose: case servewaf.DispositionAllow, servewaf.DispositionDeny, servewaf.DispositionClose:
rq.disposition = disposition rq.disposition = disposition
rq.answered = true rq.answered = true
rq.asn = answer.header.Get(asnHeader) rq.asn = answer.header.Get(asnHeader)
@@ -74,7 +74,7 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
rq.disposition = rq.h.config.DefaultDisposition rq.disposition = rq.h.config.DefaultDisposition
rq.byDefault = true rq.byDefault = true
if rq.disposition == proxy.DispositionAllow { if rq.disposition == servewaf.DispositionAllow {
rq.passToApp(ctx) rq.passToApp(ctx)
} else { } else {
rq.answer(http.StatusServiceUnavailable) rq.answer(http.StatusServiceUnavailable)
@@ -169,13 +169,13 @@ func (rq *request) newQuestion(ctx context.Context) (*http.Request, error) {
question.TransferEncoding = in.TransferEncoding question.TransferEncoding = in.TransferEncoding
question.Header = in.Header.Clone() question.Header = in.Header.Clone()
question.Header.Set(proxy.ClientHeader, rq.client.String()) question.Header.Set(servewaf.ClientHeader, rq.client.String())
question.Header.Set(proxy.PeerHeader, rq.peer.String()) question.Header.Set(servewaf.PeerHeader, rq.peer.String())
question.Header.Del(proxy.ContentLengthHeader) question.Header.Del(servewaf.ContentLengthHeader)
announced := in.Header.Get("Content-Length") announced := in.Header.Get("Content-Length")
if announced != "" { if announced != "" {
question.Header.Set(proxy.ContentLengthHeader, announced) question.Header.Set(servewaf.ContentLengthHeader, announced)
} }
question.Header.Set(requestIDHeader, rq.id) question.Header.Set(requestIDHeader, rq.id)
@@ -196,7 +196,7 @@ func (rq *request) answerWith(answer *servewafAnswer) {
header := rq.out.Header() header := rq.out.Header()
maps.Copy(header, answer.header) maps.Copy(header, answer.header)
header.Del(proxy.DispositionHeader) header.Del(servewaf.DispositionHeader)
// Go's server adds a Content-Type it guesses from the body to a response whose // Go's server adds a Content-Type it guesses from the body to a response whose
// headers have no Content-Type key. The key with no value sends none. // headers have no Content-Type key. The key with no value sends none.
@@ -210,7 +210,7 @@ func (rq *request) answerWith(answer *servewafAnswer) {
// sendReport sends servewaf report on a goroutine of its own, so that the // sendReport sends servewaf report on a goroutine of its own, so that the
// visitor does not wait for it. The report goes on after ctx, the request's own // visitor does not wait for it. The report goes on after ctx, the request's own
// context, has ended. // context, has ended.
func (h *handler) sendReport(ctx context.Context, report *proxy.Report) { func (h *handler) sendReport(ctx context.Context, report *servewaf.Report) {
h.reportsMu.Lock() h.reportsMu.Lock()
defer h.reportsMu.Unlock() defer h.reportsMu.Unlock()
@@ -228,7 +228,7 @@ func (h *handler) sendReport(ctx context.Context, report *proxy.Report) {
// postReport posts report to servewaf, and waits for its answer for up to // postReport posts report to servewaf, and waits for its answer for up to
// SWRP_TIMEOUT_MS. // SWRP_TIMEOUT_MS.
func (h *handler) postReport(ctx context.Context, report *proxy.Report) error { func (h *handler) postReport(ctx context.Context, report *servewaf.Report) error {
body, err := json.Marshal(report) body, err := json.Marshal(report)
if err != nil { if err != nil {
return fmt.Errorf("encode the report: %w", err) return fmt.Errorf("encode the report: %w", err)
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"bytes" "bytes"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"encoding/json" "encoding/json"
@@ -10,9 +10,9 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
"sneak.berlin/go/smallwebwaf/internal/state" "sneak.berlin/go/smallwebwaf/internal/state"
) )
@@ -88,11 +88,11 @@ func TestAdminEndpointsNeedTheAdminToken(t *testing.T) {
// Any other request under /_smallwebwaf/ is not found. // Any other request under /_smallwebwaf/ is not found.
for _, e := range []adminEndpoint{ for _, e := range []adminEndpoint{
{http.MethodPut, proxy.BansPath, banOtherClient}, {http.MethodPut, servewaf.BansPath, banOtherClient},
{http.MethodDelete, proxy.BansPath, ""}, {http.MethodDelete, servewaf.BansPath, ""},
{http.MethodGet, proxy.BansPath + "/" + otherClient, ""}, {http.MethodGet, servewaf.BansPath + "/" + otherClient, ""},
{http.MethodPost, proxy.ClientsPath + otherClient, ""}, {http.MethodPost, servewaf.ClientsPath + otherClient, ""},
{http.MethodGet, strings.TrimSuffix(proxy.ClientsPath, "/"), ""}, {http.MethodGet, strings.TrimSuffix(servewaf.ClientsPath, "/"), ""},
} { } {
s.admin(e.method, e.path, e.body, http.StatusNotFound) s.admin(e.method, e.path, e.body, http.StatusNotFound)
} }
@@ -118,7 +118,7 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
Reason: "probes for logins", Reason: "probes for logins",
} }
wantBans(t, s.admin(http.MethodPost, proxy.BansPath, banOtherClient, http.StatusOK), wantBans(t, s.admin(http.MethodPost, servewaf.BansPath, banOtherClient, http.StatusOK),
want) want)
line := s.get(client, http.StatusForbidden, requestlog.ActionBanned) line := s.get(client, http.StatusForbidden, requestlog.ActionBanned)
@@ -129,7 +129,7 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
// Its notes count the request it refused. // Its notes count the request it refused.
want.Notes.Requests, want.Notes.Refused = 1, 1 want.Notes.Requests, want.Notes.Refused = 1, 1
wantBans(t, s.admin(http.MethodGet, proxy.BansPath, "", http.StatusOK), want) wantBans(t, s.admin(http.MethodGet, servewaf.BansPath, "", http.StatusOK), want)
// Ten minutes on, lifting the bans on client lifts that one, which is // Ten minutes on, lifting the bans on client lifts that one, which is
// kept, marked lifted. // kept, marked lifted.
@@ -138,13 +138,13 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
lifted := clk.Now() lifted := clk.Now()
want.Lifted = &lifted want.Lifted = &lifted
wantBans(t, s.admin(http.MethodDelete, proxy.BansPath+"/"+client, "", wantBans(t, s.admin(http.MethodDelete, servewaf.BansPath+"/"+client, "",
http.StatusOK), want) http.StatusOK), want)
s.get(client, http.StatusOK, requestlog.ActionForward) s.get(client, http.StatusOK, requestlog.ActionForward)
wantBans(t, s.admin(http.MethodGet, proxy.BansPath, "", http.StatusOK), want) wantBans(t, s.admin(http.MethodGet, servewaf.BansPath, "", http.StatusOK), want)
// No ban on it is active any more. // No ban on it is active any more.
s.admin(http.MethodDelete, proxy.BansPath+"/"+client, "", http.StatusNotFound) s.admin(http.MethodDelete, servewaf.BansPath+"/"+client, "", http.StatusNotFound)
} }
func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) { func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
@@ -181,7 +181,7 @@ func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
want.Expires = &expires want.Expires = &expires
} }
wantBans(t, s.admin(http.MethodPost, proxy.BansPath, body, http.StatusOK), want) wantBans(t, s.admin(http.MethodPost, servewaf.BansPath, body, http.StatusOK), want)
} }
} }
@@ -250,7 +250,7 @@ func TestBanToAddThatCannotBeReadIsRefused(t *testing.T) {
"request body too large", "request body too large",
}, },
} { } {
got := s.admin(http.MethodPost, proxy.BansPath, tc.body, http.StatusBadRequest) got := s.admin(http.MethodPost, servewaf.BansPath, tc.body, http.StatusBadRequest)
if !strings.Contains(string(got.body), tc.want) { if !strings.Contains(string(got.body), tc.want) {
t.Errorf("%.80s was answered %q, want it to say %q", tc.body, got.body, tc.want) t.Errorf("%.80s was answered %q, want it to say %q", tc.body, got.body, tc.want)
} }
@@ -283,7 +283,7 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
Bans []state.BanEntry `json:"bans"` Bans []state.BanEntry `json:"bans"`
} }
decode(t, s.admin(http.MethodGet, proxy.ClientsPath+addr, "", http.StatusOK), &got) decode(t, s.admin(http.MethodGet, servewaf.ClientsPath+addr, "", http.StatusOK), &got)
if got.Client == nil { if got.Client == nil {
t.Fatalf("%s: no client", addr) t.Fatalf("%s: no client", addr)
@@ -305,13 +305,13 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
// Of an address no request came from and no ban covers, nothing is // Of an address no request came from and no ban covers, nothing is
// known. // known.
got := s.admin(http.MethodGet, proxy.ClientsPath+"198.51.100.99", "", http.StatusOK) got := s.admin(http.MethodGet, servewaf.ClientsPath+"198.51.100.99", "", http.StatusOK)
if string(got.body) != "{\n \"client\": null,\n \"bans\": []\n}\n" { if string(got.body) != "{\n \"client\": null,\n \"bans\": []\n}\n" {
t.Errorf("an unknown client is answered\n%s", got.body) t.Errorf("an unknown client is answered\n%s", got.body)
} }
s.admin(http.MethodGet, proxy.ClientsPath+"203.0.113", "", http.StatusBadRequest) s.admin(http.MethodGet, servewaf.ClientsPath+"203.0.113", "", http.StatusBadRequest)
s.admin(http.MethodDelete, proxy.BansPath+"/203.0.113.0/24", "", s.admin(http.MethodDelete, servewaf.BansPath+"/203.0.113.0/24", "",
http.StatusBadRequest) http.StatusBadRequest)
} }
@@ -342,10 +342,10 @@ type adminEndpoint struct {
// bans on otherClient, and asking about otherClient. // bans on otherClient, and asking about otherClient.
func adminEndpoints() []adminEndpoint { func adminEndpoints() []adminEndpoint {
return []adminEndpoint{ return []adminEndpoint{
{http.MethodGet, proxy.BansPath, ""}, {http.MethodGet, servewaf.BansPath, ""},
{http.MethodPost, proxy.BansPath, banOtherClient}, {http.MethodPost, servewaf.BansPath, banOtherClient},
{http.MethodDelete, proxy.BansPath + "/" + otherClient, ""}, {http.MethodDelete, servewaf.BansPath + "/" + otherClient, ""},
{http.MethodGet, proxy.ClientsPath + otherClient, ""}, {http.MethodGet, servewaf.ClientsPath + otherClient, ""},
} }
} }
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"maps" "maps"
@@ -10,8 +10,8 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -201,7 +201,7 @@ func TestObserveModeWorksOutABanOnlyWhenItsAlertWouldBeSent(t *testing.T) {
// never sent them, and returns the queue they wait in as well. // never sent them, and returns the queue they wait in as well.
func startWithAlerts( func startWithAlerts(
t *testing.T, env map[string]string, t *testing.T, env map[string]string,
) (*sender, *clock, *proxy.Server, *alerts.Queue) { ) (*sender, *clock, *servewaf.Server, *alerts.Queue) {
t.Helper() t.Helper()
return startAppWithAlerts(t, func(http.ResponseWriter, *http.Request) {}, env) return startAppWithAlerts(t, func(http.ResponseWriter, *http.Request) {}, env)
@@ -210,7 +210,7 @@ func startWithAlerts(
// startAppWithAlerts is startWithAlerts in front of the app handler. // startAppWithAlerts is startWithAlerts in front of the app handler.
func startAppWithAlerts( func startAppWithAlerts(
t *testing.T, handler http.HandlerFunc, env map[string]string, t *testing.T, handler http.HandlerFunc, env map[string]string,
) (*sender, *clock, *proxy.Server, *alerts.Queue) { ) (*sender, *clock, *servewaf.Server, *alerts.Queue) {
t.Helper() t.Helper()
app := startApp(t, handler) app := startApp(t, handler)
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"net/netip" "net/netip"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"maps" "maps"
@@ -13,9 +13,9 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/anomaly" "sneak.berlin/go/smallwebwaf/internal/anomaly"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// The anomaly thresholds: the prefix of a scope followed by the end of a // The anomaly thresholds: the prefix of a scope followed by the end of a
@@ -319,7 +319,7 @@ func TestRequestCountsForTheASNumberGeoJSGivesBeforeItEnds(t *testing.T) {
// kept. // kept.
geojsURL, _, release := startHeldGeoJS(t) geojsURL, _, release := startHeldGeoJS(t)
var server atomic.Pointer[proxy.Server] var server atomic.Pointer[servewaf.Server]
app := startApp(t, func(http.ResponseWriter, *http.Request) { app := startApp(t, func(http.ResponseWriter, *http.Request) {
release() release()
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"fmt" "fmt"
@@ -16,10 +16,10 @@ import (
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/reputation" "sneak.berlin/go/smallwebwaf/internal/reputation"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -81,7 +81,7 @@ func TestSixteenThousandCookiesAreRefusedWellWithinTheTimeLimit(t *testing.T) {
t.Errorf("answered after %s, want well within %s", took, answerTimeLimit) t.Errorf("answered after %s, want well within %s", took, answerTimeLimit)
} }
wantDisposition(t, got, tooLarge, proxy.DispositionDeny) wantDisposition(t, got, tooLarge, servewaf.DispositionDeny)
line := s.out.requestLine(t) line := s.out.requestLine(t)
wantLine(t, line, tooLarge, requestlog.ActionTooLarge) wantLine(t, line, tooLarge, requestlog.ActionTooLarge)
@@ -144,13 +144,13 @@ func TestEachHeaderLimitRefusesARequestOverIt(t *testing.T) {
line := s.out.requestLine(t) line := s.out.requestLine(t)
if tc.refused { if tc.refused {
wantDisposition(t, got, tooLarge, proxy.DispositionDeny) wantDisposition(t, got, tooLarge, servewaf.DispositionDeny)
wantLine(t, line, tooLarge, requestlog.ActionTooLarge) wantLine(t, line, tooLarge, requestlog.ActionTooLarge)
return return
} }
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
wantLine(t, line, http.StatusOK, requestlog.ActionForward) wantLine(t, line, http.StatusOK, requestlog.ActionForward)
if line.DurationWAF == nil { if line.DurationWAF == nil {
@@ -175,7 +175,7 @@ func TestHeaderLimitsHoldWhateverTheCoreRuleSetSettingsSay(t *testing.T) {
req := s.question(siteA, client, "/") req := s.question(siteA, client, "/")
req.Header.Set(cookieHeader, cookies(cookieLimit+1)) req.Header.Set(cookieHeader, cookies(cookieLimit+1))
wantDisposition(t, do(t, req), tooLarge, proxy.DispositionDeny) wantDisposition(t, do(t, req), tooLarge, servewaf.DispositionDeny)
}) })
} }
} }
@@ -187,7 +187,7 @@ func TestClientInTheAllowNetsSkipsTheHeaderLimits(t *testing.T) {
req := s.question(siteA, client, "/") req := s.question(siteA, client, "/")
req.Header.Set(cookieHeader, cookies(cookieLimit+1)) req.Header.Set(cookieHeader, cookies(cookieLimit+1))
wantDisposition(t, do(t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, req), http.StatusOK, servewaf.DispositionAllow)
} }
func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) { func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
@@ -204,7 +204,7 @@ func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
t.Errorf("answered after %s, want well within %s", took, answerTimeLimit) t.Errorf("answered after %s, want well within %s", took, answerTimeLimit)
} }
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
line := s.out.requestLine(t) line := s.out.requestLine(t)
if line.WouldAction != requestlog.ActionTooLarge || line.DurationWAF != nil { if line.WouldAction != requestlog.ActionTooLarge || line.DurationWAF != nil {
@@ -224,8 +224,8 @@ func TestRequestsOverTheHeaderLimitsCountTowardTheRateLimits(t *testing.T) {
return do(t, req) return do(t, req)
} }
wantDisposition(t, ask(), tooLarge, proxy.DispositionDeny) wantDisposition(t, ask(), tooLarge, servewaf.DispositionDeny)
wantDisposition(t, ask(), http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, ask(), http.StatusForbidden, servewaf.DispositionDeny)
lines := s.out.requestLines(t, 2) lines := s.out.requestLines(t, 2)
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionRateLimited) wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionRateLimited)
@@ -249,7 +249,7 @@ func TestTheHeaderLimitsComeAfterTheTrapPathsAndTheRuleFiles(t *testing.T) {
req := s.question(siteA, client, tc.path) req := s.question(siteA, client, tc.path)
req.Header.Set(cookieHeader, cookies(cookieLimit+1)) req.Header.Set(cookieHeader, cookies(cookieLimit+1))
wantDisposition(t, do(t, req), http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, do(t, req), http.StatusForbidden, servewaf.DispositionDeny)
wantLine(t, s.out.requestLine(t), http.StatusForbidden, tc.action) wantLine(t, s.out.requestLine(t), http.StatusForbidden, tc.action)
}) })
} }
@@ -265,7 +265,7 @@ func TestNotWaitingForGeoJSNeverLowersAClientsLimit(t *testing.T) {
req := s.question(siteA, fromDE, "/") req := s.question(siteA, fromDE, "/")
req.Header.Set(cookieHeader, cookies(cookieLimit+1)) req.Header.Set(cookieHeader, cookies(cookieLimit+1))
wantDisposition(t, do(t, req), tooLarge, proxy.DispositionDeny) wantDisposition(t, do(t, req), tooLarge, servewaf.DispositionDeny)
line := s.out.requestLine(t) line := s.out.requestLine(t)
wantLine(t, line, tooLarge, requestlog.ActionTooLarge) wantLine(t, line, tooLarge, requestlog.ActionTooLarge)
@@ -275,7 +275,7 @@ func TestNotWaitingForGeoJSNeverLowersAClientsLimit(t *testing.T) {
} }
// The client is under no ban, and GeoJS places it in a country. // The client is under no ban, and GeoJS places it in a country.
wantDisposition(t, s.ask(fromDE, "/"), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, s.ask(fromDE, "/"), http.StatusOK, servewaf.DispositionAllow)
} }
func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T) { func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T) {
@@ -291,7 +291,7 @@ func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T
req := s.question(siteA, client, "/") req := s.question(siteA, client, "/")
req.Header.Set(cookieHeader, cookies(cookieLimit+1)) req.Header.Set(cookieHeader, cookies(cookieLimit+1))
wantDisposition(t, do(t, req), http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, do(t, req), http.StatusForbidden, servewaf.DispositionDeny)
wantLine(t, s.out.requestLine(t), http.StatusForbidden, requestlog.ActionRateLimited) wantLine(t, s.out.requestLine(t), http.StatusForbidden, requestlog.ActionRateLimited)
} }
@@ -306,7 +306,7 @@ func TestObserveModeWaitsForGeoJSForARequestOverTheHeaderLimits(t *testing.T) {
req.Header.Set(cookieHeader, cookies(cookieLimit+1)) req.Header.Set(cookieHeader, cookies(cookieLimit+1))
got := do(t, req) got := do(t, req)
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
asn, country := got.header.Get("X-Client-Asn"), got.header.Get("X-Client-Country") asn, country := got.header.Get("X-Client-Asn"), got.header.Get("X-Client-Country")
if asn != asnDE || country != "DE" { if asn != asnDE || country != "DE" {
@@ -318,7 +318,9 @@ func TestObserveModeWaitsForGeoJSForARequestOverTheHeaderLimits(t *testing.T) {
// newServeWAFInBubble makes a servewaf for a synctest bubble: GeoJS never // newServeWAFInBubble makes a servewaf for a synctest bubble: GeoJS never
// answers, a setting needs its answer, and everything is at its default but // answers, a setting needs its answer, and everything is at its default but
// for env. No request to it may use the network. // for env. No request to it may use the network.
func newServeWAFInBubble(t *testing.T, env map[string]string) (*proxy.Server, *output) { func newServeWAFInBubble(
t *testing.T, env map[string]string,
) (*servewaf.Server, *output) {
t.Helper() t.Helper()
settings := map[string]string{deniedCountries: "KP"} settings := map[string]string{deniedCountries: "KP"}
@@ -332,13 +334,13 @@ func newServeWAFInBubble(t *testing.T, env map[string]string) (*proxy.Server, *o
// askDirectly hands a question about a request from client straight to server, // askDirectly hands a question about a request from client straight to server,
// as change leaves it, and returns the answer's status and how long it took. // as change leaves it, and returns the answer's status and how long it took.
func askDirectly( func askDirectly(
t *testing.T, server *proxy.Server, change func(*http.Request), t *testing.T, server *servewaf.Server, change func(*http.Request),
) (int, time.Duration) { ) (int, time.Duration) {
t.Helper() t.Helper()
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody) req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
req.RemoteAddr = net.JoinHostPort(rproxyPeer, "1234") req.RemoteAddr = net.JoinHostPort(rproxyPeer, "1234")
req.Header.Set(proxy.ClientHeader, client) req.Header.Set(servewaf.ClientHeader, client)
req.Header.Set(requestIDHeader, "the-id") req.Header.Set(requestIDHeader, "the-id")
change(req) change(req)
@@ -351,17 +353,17 @@ func askDirectly(
} }
// banClient bans client at now. // banClient bans client at now.
func banClient(_ *testing.T, server *proxy.Server, now time.Time) { func banClient(_ *testing.T, server *servewaf.Server, now time.Time) {
server.Ledger.BanForLimit(netip.MustParsePrefix(client+"/32"), now, bans.Notes{}) server.Ledger.BanForLimit(netip.MustParsePrefix(client+"/32"), now, bans.Notes{})
} }
// countClient counts one request from client at now for the rate limits. // countClient counts one request from client at now for the rate limits.
func countClient(_ *testing.T, server *proxy.Server, now time.Time) { func countClient(_ *testing.T, server *servewaf.Server, now time.Time) {
server.Limiter.Count(netip.MustParsePrefix(client+"/32"), now, wholeLimit) server.Limiter.Count(netip.MustParsePrefix(client+"/32"), now, wholeLimit)
} }
// listClient puts client on the blocklist at dropURL. // listClient puts client on the blocklist at dropURL.
func listClient(t *testing.T, server *proxy.Server, now time.Time) { func listClient(t *testing.T, server *servewaf.Server, now time.Time) {
t.Helper() t.Helper()
err := server.Lists.Load([]reputation.List{{ err := server.Lists.Load([]reputation.List{{
@@ -373,13 +375,13 @@ func listClient(t *testing.T, server *proxy.Server, now time.Time) {
} }
// decideOnClient puts client on the CrowdSec decision list. // decideOnClient puts client on the CrowdSec decision list.
func decideOnClient(t *testing.T, server *proxy.Server, now time.Time) { func decideOnClient(t *testing.T, server *servewaf.Server, now time.Time) {
t.Helper() t.Helper()
loadDecisions(t, server, now, clientsDecision) loadDecisions(t, server, now, clientsDecision)
} }
// leaveAsItIs prepares nothing. // leaveAsItIs prepares nothing.
func leaveAsItIs(*testing.T, *proxy.Server, time.Time) {} func leaveAsItIs(*testing.T, *servewaf.Server, time.Time) {}
func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) { func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
t.Parallel() t.Parallel()
@@ -390,7 +392,7 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
name string name string
env map[string]string env map[string]string
// prepare gets the server ready, at now, before the question. // prepare gets the server ready, at now, before the question.
prepare func(t *testing.T, server *proxy.Server, now time.Time) prepare func(t *testing.T, server *servewaf.Server, now time.Time)
// cookie is the request's Cookie header. // cookie is the request's Cookie header.
cookie string cookie string
status int status int
@@ -461,7 +463,7 @@ func TestAQuestionWaitsForASlowGeoJSOnlyAsLongAsTheDefaultSays(t *testing.T) {
name string name string
env map[string]string env map[string]string
// prepare gets the server ready, at now, before the question. // prepare gets the server ready, at now, before the question.
prepare func(t *testing.T, server *proxy.Server, now time.Time) prepare func(t *testing.T, server *servewaf.Server, now time.Time)
status int status int
action string action string
}{ }{
@@ -519,10 +521,10 @@ func TestServeWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(t *testin
t.Cleanup(func() { _ = resolver.Close() }) t.Cleanup(func() { _ = resolver.Close() })
server, out, _ := newServeWAFWith(t, func(params proxy.Params) *proxy.Server { server, out, _ := newServeWAFWith(t, func(params servewaf.Params) *servewaf.Server {
params.AbuseIPDBURL = silentAbuseIPDBURL params.AbuseIPDBURL = silentAbuseIPDBURL
return proxy.NewServeWAF(params) return servewaf.New(params)
}, held, time.Now, map[string]string{ }, held, time.Now, map[string]string{
deniedCountries: "KP", wafMode: block, deniedCountries: "KP", wafMode: block,
dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(), dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(),
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"net/http" "net/http"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"bufio" "bufio"
@@ -15,8 +15,8 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -382,7 +382,7 @@ func (c *clock) advance(d time.Duration) {
// window. // window.
func startWithClock( func startWithClock(
t *testing.T, geojsURL string, env map[string]string, t *testing.T, geojsURL string, env map[string]string,
) (*sender, *clock, *proxy.Server) { ) (*sender, *clock, *servewaf.Server) {
t.Helper() t.Helper()
app := startApp(t, func(http.ResponseWriter, *http.Request) {}) app := startApp(t, func(http.ResponseWriter, *http.Request) {})
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"fmt" "fmt"
@@ -16,8 +16,8 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest" "sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// The biased thresholds. // The biased thresholds.
@@ -419,7 +419,7 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(t *testing.T
// clock. // clock.
func startWithLookups( func startWithLookups(
t *testing.T, env map[string]string, t *testing.T, env map[string]string,
) (*sender, *proxy.Server, *alerts.Queue) { ) (*sender, *servewaf.Server, *alerts.Queue) {
t.Helper() t.Helper()
s, _, server, queue := startWithLookupsAndClock(t, env) s, _, server, queue := startWithLookupsAndClock(t, env)
@@ -434,7 +434,7 @@ func startWithLookups(
// country, and no other address. // country, and no other address.
func startWithLookupsAndClock( func startWithLookupsAndClock(
t *testing.T, env map[string]string, t *testing.T, env map[string]string,
) (*sender, *clock, *proxy.Server, *alerts.Queue) { ) (*sender, *clock, *servewaf.Server, *alerts.Queue) {
t.Helper() t.Helper()
path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb") path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb")
@@ -463,11 +463,11 @@ func (s *sender) uploadFrom(from string) logLine {
// serveFromDE hands a question about a request from fromDE with method and body // serveFromDE hands a question about a request from fromDE with method and body
// straight to server's handler, without the network, and returns once it is // straight to server's handler, without the network, and returns once it is
// answered. // answered.
func serveFromDE(t *testing.T, server *proxy.Server, method string, body io.Reader) { func serveFromDE(t *testing.T, server *servewaf.Server, method string, body io.Reader) {
t.Helper() t.Helper()
req := httptest.NewRequestWithContext(t.Context(), method, "/", body) req := httptest.NewRequestWithContext(t.Context(), method, "/", body)
req.Header.Set(proxy.ClientHeader, fromDE) req.Header.Set(servewaf.ClientHeader, fromDE)
req.Header.Set(requestIDHeader, "the-id") req.Header.Set(requestIDHeader, "the-id")
server.Handler.ServeHTTP(httptest.NewRecorder(), req) server.Handler.ServeHTTP(httptest.NewRecorder(), req)
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"bufio" "bufio"
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"crypto/rand" "crypto/rand"
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"time" "time"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"io" "io"
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"slices" "slices"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"encoding/json" "encoding/json"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
@@ -9,9 +9,9 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/reputation" "sneak.berlin/go/smallwebwaf/internal/reputation"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// The CrowdSec settings, and the tests' engine, which is never asked: each // The CrowdSec settings, and the tests' engine, which is never asked: each
@@ -168,7 +168,7 @@ func TestObserveModeForwardsAClientTheCrowdSecDecisionListListsAndAlertsTheBan(
// decisionsURL, answer, the engine's answer, fetched at fetched, as // decisionsURL, answer, the engine's answer, fetched at fetched, as
// reputation.json would at start. // reputation.json would at start.
func loadDecisions( func loadDecisions(
t *testing.T, server *proxy.Server, fetched time.Time, answer string, t *testing.T, server *servewaf.Server, fetched time.Time, answer string,
) { ) {
t.Helper() t.Helper()
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"maps" "maps"
@@ -10,9 +10,9 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const errorBurstThreshold = "SWWAF_ERROR_BURST_THRESHOLD" const errorBurstThreshold = "SWWAF_ERROR_BURST_THRESHOLD"
@@ -51,7 +51,7 @@ func (r refused) send(s *sender, from string) logLine {
// SWWAF_ERROR_BURST_THRESHOLD at threshold, and the settings in env. // SWWAF_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
func startForErrorBurst( func startForErrorBurst(
t *testing.T, threshold string, env map[string]string, t *testing.T, threshold string, env map[string]string,
) (*sender, *clock, *proxy.Server) { ) (*sender, *clock, *servewaf.Server) {
t.Helper() t.Helper()
settings := map[string]string{ settings := map[string]string{
@@ -1,11 +1,11 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
"testing" "testing"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
func TestHealthEndpointCountsTowardNoLimit(t *testing.T) { func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
@@ -21,7 +21,7 @@ func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
) )
for range healthChecks { for range healthChecks {
got := s.adminRequest("", http.MethodGet, proxy.HealthPath, "", http.StatusOK) got := s.adminRequest("", http.MethodGet, servewaf.HealthPath, "", http.StatusOK)
if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType { if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType {
t.Errorf("health endpoint answered %q with Content-Type %q, want ok "+ t.Errorf("health endpoint answered %q with Content-Type %q, want ok "+
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"io" "io"
@@ -8,9 +8,9 @@ import (
"testing" "testing"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) { func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
@@ -100,8 +100,8 @@ func TestOwnEndpointsAreNotInTheHistory(t *testing.T) {
s, _, server := startWithClock(t, "", map[string]string{metricsToken: token}) s, _, server := startWithClock(t, "", map[string]string{metricsToken: token})
s.adminRequest("", http.MethodGet, proxy.HealthPath, "", http.StatusOK) s.adminRequest("", http.MethodGet, servewaf.HealthPath, "", http.StatusOK)
s.adminRequest("", http.MethodGet, proxy.MetricsPath, "", http.StatusUnauthorized) s.adminRequest("", http.MethodGet, servewaf.MetricsPath, "", http.StatusUnauthorized)
s.scrape() s.scrape()
if clients := server.Limiter.Snapshot(); len(clients) != 0 { if clients := server.Limiter.Snapshot(); len(clients) != 0 {
@@ -126,7 +126,7 @@ func TestPathUnderTheOwnPrefixIsNeitherForwardedNorRefused(t *testing.T) {
} }
// historyOf returns the history of the client at addr. // historyOf returns the history of the client at addr.
func historyOf(t *testing.T, server *proxy.Server, addr string) ratelimit.History { func historyOf(t *testing.T, server *servewaf.Server, addr string) ratelimit.History {
t.Helper() t.Helper()
client := netip.MustParsePrefix(addr + "/32") client := netip.MustParsePrefix(addr + "/32")
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
@@ -379,7 +379,7 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
// clock of the test's own: the wait lasts exactly as long as it should, // clock of the test's own: the wait lasts exactly as long as it should,
// however slowly the test process runs. Nothing in it may wait on the // however slowly the test process runs. Nothing in it may wait on the
// network, which would keep that clock from moving on: the request is // network, which would keep that clock from moving on: the request is
// handed to the proxy's handler, and GeoJS is one that never answers. // handed to servewaf's handler, and GeoJS is one that never answers.
synctest.Test(t, func(t *testing.T) { synctest.Test(t, func(t *testing.T) {
// Not the default. The exclusive list needs the answer, and the app // Not the default. The exclusive list needs the answer, and the app
// is never reached. // is never reached.
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"io" "io"
@@ -12,8 +12,8 @@ import (
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -32,7 +32,7 @@ func TestMetricsAreOffWhileTheTokenIsUnset(t *testing.T) {
// An empty token does not match the unset one either. // An empty token does not match the unset one either.
for _, authorization := range []string{bearer, "Bearer ", ""} { for _, authorization := range []string{bearer, "Bearer ", ""} {
s.adminRequest(authorization, http.MethodGet, proxy.MetricsPath, "", s.adminRequest(authorization, http.MethodGet, servewaf.MetricsPath, "",
http.StatusNotFound) http.StatusNotFound)
} }
} }
@@ -46,18 +46,18 @@ func TestMetricsNeedTheTokenAndOtherPathsAreNotFound(t *testing.T) {
method, path, authorization string method, path, authorization string
status int status int
}{ }{
{http.MethodGet, proxy.MetricsPath, "", http.StatusUnauthorized}, {http.MethodGet, servewaf.MetricsPath, "", http.StatusUnauthorized},
{ {
http.MethodGet, proxy.MetricsPath, "Bearer " + strings.ToUpper(token), http.MethodGet, servewaf.MetricsPath, "Bearer " + strings.ToUpper(token),
http.StatusUnauthorized, http.StatusUnauthorized,
}, },
{http.MethodGet, proxy.MetricsPath, "Basic " + token, http.StatusUnauthorized}, {http.MethodGet, servewaf.MetricsPath, "Basic " + token, http.StatusUnauthorized},
{http.MethodGet, proxy.MetricsPath, bearer, http.StatusOK}, {http.MethodGet, servewaf.MetricsPath, bearer, http.StatusOK},
{http.MethodGet, proxy.MetricsPath, "bearer " + token, http.StatusOK}, {http.MethodGet, servewaf.MetricsPath, "bearer " + token, http.StatusOK},
{http.MethodPost, proxy.MetricsPath, bearer, http.StatusNotFound}, {http.MethodPost, servewaf.MetricsPath, bearer, http.StatusNotFound},
{http.MethodGet, proxy.MetricsPath + "/", bearer, http.StatusNotFound}, {http.MethodGet, servewaf.MetricsPath + "/", bearer, http.StatusNotFound},
{http.MethodGet, "/_smallwebwaf/bans", bearer, http.StatusNotFound}, {http.MethodGet, "/_smallwebwaf/bans", bearer, http.StatusNotFound},
{http.MethodPost, proxy.HealthPath, "", http.StatusNotFound}, {http.MethodPost, servewaf.HealthPath, "", http.StatusNotFound},
} { } {
got := s.adminRequest(tc.authorization, tc.method, tc.path, "", tc.status) got := s.adminRequest(tc.authorization, tc.method, tc.path, "", tc.status)
@@ -180,7 +180,7 @@ func TestMetricsCountTheBansAnAdminMakes(t *testing.T) {
wantMetric(t, s.scrape(), admins, 1) wantMetric(t, s.scrape(), admins, 1)
// And a ban made through the endpoint. // And a ban made through the endpoint.
s.admin(http.MethodPost, proxy.BansPath, banOtherClient, http.StatusOK) s.admin(http.MethodPost, servewaf.BansPath, banOtherClient, http.StatusOK)
wantMetric(t, s.scrape(), admins, 2) wantMetric(t, s.scrape(), admins, 2)
} }
@@ -365,7 +365,7 @@ func keptAnswer(addr, country string) lookup.Answer {
func scrape(t *testing.T, addr string) string { func scrape(t *testing.T, addr string) string {
t.Helper() t.Helper()
req := newRequest(t, http.MethodGet, addr, proxy.MetricsPath, http.NoBody) req := newRequest(t, http.MethodGet, addr, servewaf.MetricsPath, http.NoBody)
req.Header.Set("Authorization", bearer) req.Header.Set("Authorization", bearer)
got := do(t, req) got := do(t, req)
@@ -380,7 +380,7 @@ func scrape(t *testing.T, addr string) string {
func (s *sender) scrape() string { func (s *sender) scrape() string {
s.t.Helper() s.t.Helper()
return string(s.adminRequest(bearer, http.MethodGet, proxy.MetricsPath, "", return string(s.adminRequest(bearer, http.MethodGet, servewaf.MetricsPath, "",
http.StatusOK).body) http.StatusOK).body)
} }
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
@@ -1,10 +1,7 @@
// Package proxy is servewaf's server. It runs the checks on the request each of // Package servewaf is the server the servewaf command runs. It answers rproxy's
// rproxy's questions carries, and answers rproxy what to do with that request. It // questions, takes rproxy's reports and answers its own endpoints, and writes a
// takes rproxy's report once the visitor's answer has ended, which counts the // log line for each.
// request's bytes and writes its last log line. It answers servewaf's own package servewaf
// endpoints: the health check, the metrics, the bans and the clients. It passes
// nothing to an app: internal/rproxy does that.
package proxy
import ( import (
"io" "io"
@@ -46,7 +43,7 @@ const BansPath = "/_smallwebwaf/bans"
// client's address after it, with SWWAF_ADMIN_TOKEN. // client's address after it, with SWWAF_ADMIN_TOKEN.
const ClientsPath = "/_smallwebwaf/clients/" const ClientsPath = "/_smallwebwaf/clients/"
// Params are what NewServeWAF needs. // Params are what New needs.
type Params struct { type Params struct {
Config *config.Config Config *config.Config
// RequestLog receives two JSON lines per question, one at its decision and // RequestLog receives two JSON lines per question, one at its decision and
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"bufio" "bufio"
@@ -18,10 +18,10 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rproxy" "sneak.berlin/go/smallwebwaf/internal/rproxy"
"sneak.berlin/go/smallwebwaf/internal/rules" "sneak.berlin/go/smallwebwaf/internal/rules"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -300,7 +300,7 @@ func startProxyWithGeoJS(
func startProxyWithClock( func startProxyWithClock(
t *testing.T, appURL, geojsURL string, now func() time.Time, t *testing.T, appURL, geojsURL string, now func() time.Time,
env map[string]string, env map[string]string,
) (string, *output, *proxy.Server) { ) (string, *output, *servewaf.Server) {
t.Helper() t.Helper()
addr, out, server, _ := startProxyWithAlerts(t, appURL, geojsURL, now, env) addr, out, server, _ := startProxyWithAlerts(t, appURL, geojsURL, now, env)
@@ -313,7 +313,7 @@ func startProxyWithClock(
func startProxyWithAlerts( func startProxyWithAlerts(
t *testing.T, appURL, geojsURL string, now func() time.Time, t *testing.T, appURL, geojsURL string, now func() time.Time,
env map[string]string, env map[string]string,
) (string, *output, *proxy.Server, *alerts.Queue) { ) (string, *output, *servewaf.Server, *alerts.Queue) {
t.Helper() t.Helper()
server, out, alertQueue := newServeWAF(t, geojsURL, now, env) server, out, alertQueue := newServeWAF(t, geojsURL, now, env)
@@ -324,7 +324,7 @@ func startProxyWithAlerts(
// serve serves server on a port of its own until the test ends, and sets its // serve serves server on a port of its own until the test ends, and sets its
// Addr to where it listens. // Addr to where it listens.
func serve(t *testing.T, server *proxy.Server) { func serve(t *testing.T, server *servewaf.Server) {
t.Helper() t.Helper()
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0") listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
@@ -401,17 +401,17 @@ func startRProxy(
// inspected by it. // inspected by it.
func newServeWAF( func newServeWAF(
t *testing.T, geojsURL string, now func() time.Time, env map[string]string, t *testing.T, geojsURL string, now func() time.Time, env map[string]string,
) (*proxy.Server, *output, *alerts.Queue) { ) (*servewaf.Server, *output, *alerts.Queue) {
t.Helper() t.Helper()
return newServeWAFWith(t, proxy.NewServeWAF, geojsURL, now, env) return newServeWAFWith(t, servewaf.New, geojsURL, now, env)
} }
// newServeWAFWith is newServeWAF for the server build makes. // newServeWAFWith is newServeWAF for the server build makes.
func newServeWAFWith( func newServeWAFWith(
t *testing.T, build func(proxy.Params) *proxy.Server, t *testing.T, build func(servewaf.Params) *servewaf.Server,
geojsURL string, now func() time.Time, env map[string]string, geojsURL string, now func() time.Time, env map[string]string,
) (*proxy.Server, *output, *alerts.Queue) { ) (*servewaf.Server, *output, *alerts.Queue) {
t.Helper() t.Helper()
settings := map[string]string{ settings := map[string]string{
@@ -463,7 +463,7 @@ func newServeWAFWith(
} }
} }
server := build(proxy.Params{ server := build(servewaf.Params{
Config: cfg, Config: cfg,
RequestLog: out, RequestLog: out,
ProcessLog: processLog, ProcessLog: processLog,
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"fmt" "fmt"
@@ -15,10 +15,10 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/reputation" "sneak.berlin/go/smallwebwaf/internal/reputation"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// The reputation settings. // The reputation settings.
@@ -617,7 +617,7 @@ const (
// AbuseIPDB. // AbuseIPDB.
const abuseipdb = reputation.AbuseIPDBSource const abuseipdb = reputation.AbuseIPDBSource
// abuseIPDBURL is where newProxy has clients checked with AbuseIPDB: at // abuseIPDBURL is where newServeWAF has clients checked with AbuseIPDB: at
// abuseIPDBStandIn, which TestMain registers with Go's default transport, // abuseIPDBStandIn, which TestMain registers with Go's default transport,
// through which AbuseIPDB is asked. // through which AbuseIPDB is asked.
const abuseIPDBURL = "abuseipdb://stand-in/api/v2/check" const abuseIPDBURL = "abuseipdb://stand-in/api/v2/check"
@@ -975,7 +975,7 @@ func verdictsFetched() time.Time {
// verdictsFetched, as reputation.json would at start: one that lists the // verdictsFetched, as reputation.json would at start: one that lists the
// client from each zone listedBy gives for it, and one that does not from // client from each zone listedBy gives for it, and one that does not from
// each other zone. // each other zone.
func loadVerdicts(server *proxy.Server, listedBy map[string][]string) { func loadVerdicts(server *servewaf.Server, listedBy map[string][]string) {
verdicts := make([]reputation.Verdict, 0, len(listedBy)*len(server.DNSBL.Zones())) verdicts := make([]reputation.Verdict, 0, len(listedBy)*len(server.DNSBL.Zones()))
for client, zones := range listedBy { for client, zones := range listedBy {
@@ -993,7 +993,7 @@ func loadVerdicts(server *proxy.Server, listedBy map[string][]string) {
// loadScores puts into server's AbuseIPDB the score scores gives each // loadScores puts into server's AbuseIPDB the score scores gives each
// client, an IPv4 address, fetched at verdictsFetched, as reputation.json // client, an IPv4 address, fetched at verdictsFetched, as reputation.json
// would at start. // would at start.
func loadScores(server *proxy.Server, scores map[string]int64) { func loadScores(server *servewaf.Server, scores map[string]int64) {
kept := make([]reputation.Score, 0, len(scores)) kept := make([]reputation.Score, 0, len(scores))
for client, score := range scores { for client, score := range scores {
kept = append(kept, reputation.Score{ kept = append(kept, reputation.Score{
@@ -1007,7 +1007,7 @@ func loadScores(server *proxy.Server, scores map[string]int64) {
// wantAbuseIPDBChecks checks how many clients server has checked with // wantAbuseIPDBChecks checks how many clients server has checked with
// AbuseIPDB. // AbuseIPDB.
func wantAbuseIPDBChecks(t *testing.T, server *proxy.Server, want int) { func wantAbuseIPDBChecks(t *testing.T, server *servewaf.Server, want int) {
t.Helper() t.Helper()
if got := server.AbuseIPDB.Checked(); got != want { if got := server.AbuseIPDB.Checked(); got != want {
@@ -1017,7 +1017,7 @@ func wantAbuseIPDBChecks(t *testing.T, server *proxy.Server, want int) {
// loadLists puts copies of lists into server's lists, by URL, each with // loadLists puts copies of lists into server's lists, by URL, each with
// its lines, fetched at listsFetched, as reputation.json would at start. // its lines, fetched at listsFetched, as reputation.json would at start.
func loadLists(t *testing.T, server *proxy.Server, copies map[string][]string) { func loadLists(t *testing.T, server *servewaf.Server, copies map[string][]string) {
t.Helper() t.Helper()
lists := make([]reputation.List, 0, len(copies)) lists := make([]reputation.List, 0, len(copies))
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"io" "io"
@@ -10,9 +10,9 @@ import (
"testing" "testing"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -220,7 +220,7 @@ func TestTimingsAreInOrder(t *testing.T) {
wantStatus(t, do(t, req), http.StatusForbidden) wantStatus(t, do(t, req), http.StatusForbidden)
refused := out.endedLines(t, 2)[1] refused := out.endedLines(t, 2)[1]
wantStatus(t, get(t, server.Addr, proxy.HealthPath), http.StatusOK) wantStatus(t, get(t, server.Addr, servewaf.HealthPath), http.StatusOK)
health := out.endedLines(t, 3)[2] health := out.endedLines(t, 3)[2]
// A request passed to the app has every timing; one refused, none of // A request passed to the app has every timing; one refused, none of
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"slices" "slices"
@@ -1,4 +1,4 @@
package proxy package servewaf
import ( import (
"bytes" "bytes"
@@ -103,13 +103,13 @@ type Report struct {
DurationUpstreamTotal float64 `json:"duration_upstream_total"` DurationUpstreamTotal float64 `json:"duration_upstream_total"`
} }
// NewServeWAF returns the server `smallwebwaf servewaf` runs. It answers // New returns the server `smallwebwaf servewaf` runs. It answers
// rproxy's questions, takes rproxy's reports and answers its own endpoints. It // rproxy's questions, takes rproxy's reports and answers its own endpoints. It
// passes nothing to an app. rproxy applies the size and time limits to the // passes nothing to an app. rproxy applies the size and time limits to the
// visitor. Go's default limit on a request's line and headers, 1 MiB, stays: it // visitor. Go's default limit on a request's line and headers, 1 MiB, stays: it
// is far above rproxy's, and a question refused for its size would have rproxy // is far above rproxy's, and a question refused for its size would have rproxy
// let the request through. // let the request through.
func NewServeWAF(params Params) *Server { func New(params Params) *Server {
h := newHandler(params) h := newHandler(params)
return &Server{ return &Server{
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"context" "context"
@@ -19,9 +19,9 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/ratelimit" "sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
const ( const (
@@ -57,7 +57,7 @@ type serveWAF struct {
t *testing.T t *testing.T
addr string addr string
out *output out *output
server *proxy.Server server *servewaf.Server
clock *clock clock *clock
queue *alerts.Queue queue *alerts.Queue
} }
@@ -67,13 +67,13 @@ type serveWAF struct {
func startServeWAF(t *testing.T, geojsURL string, env map[string]string) *serveWAF { func startServeWAF(t *testing.T, geojsURL string, env map[string]string) *serveWAF {
t.Helper() t.Helper()
return startServeWAFWith(t, geojsURL, env, func(*proxy.Server) {}) return startServeWAFWith(t, geojsURL, env, func(*servewaf.Server) {})
} }
// startServeWAFWith is startServeWAF with change applied to the server before // startServeWAFWith is startServeWAF with change applied to the server before
// it starts. // it starts.
func startServeWAFWith( func startServeWAFWith(
t *testing.T, geojsURL string, env map[string]string, change func(*proxy.Server), t *testing.T, geojsURL string, env map[string]string, change func(*servewaf.Server),
) *serveWAF { ) *serveWAF {
t.Helper() t.Helper()
@@ -103,8 +103,8 @@ func (s *serveWAF) questionWith(
req := newRequest(s.t, method, s.addr, path, body) req := newRequest(s.t, method, s.addr, path, body)
req.Host = host req.Host = host
req.Header.Set(proxy.ClientHeader, from) req.Header.Set(servewaf.ClientHeader, from)
req.Header.Set(proxy.PeerHeader, rproxyPeer) req.Header.Set(servewaf.PeerHeader, rproxyPeer)
req.Header.Set(requestIDHeader, rand.Text()) req.Header.Set(requestIDHeader, rand.Text())
return req return req
@@ -131,13 +131,13 @@ func (s *serveWAF) allowed(from string) string {
s.t.Helper() s.t.Helper()
req := s.question(siteA, from, "/") req := s.question(siteA, from, "/")
wantDisposition(s.t, do(s.t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(s.t, do(s.t, req), http.StatusOK, servewaf.DispositionAllow)
return req.Header.Get(requestIDHeader) return req.Header.Get(requestIDHeader)
} }
// report sends rproxy's report and returns the answer. // report sends rproxy's report and returns the answer.
func (s *serveWAF) report(report proxy.Report) answer { func (s *serveWAF) report(report servewaf.Report) answer {
s.t.Helper() s.t.Helper()
body, err := json.Marshal(report) body, err := json.Marshal(report)
@@ -152,7 +152,7 @@ func (s *serveWAF) report(report proxy.Report) answer {
func (s *serveWAF) reportBody(body string) answer { func (s *serveWAF) reportBody(body string) answer {
s.t.Helper() s.t.Helper()
return do(s.t, newRequest(s.t, http.MethodPost, s.addr, proxy.ReportPath, return do(s.t, newRequest(s.t, http.MethodPost, s.addr, servewaf.ReportPath,
strings.NewReader(body))) strings.NewReader(body)))
} }
@@ -161,7 +161,7 @@ func (s *serveWAF) reportBody(body string) answer {
func (s *serveWAF) passedOn(from string) { func (s *serveWAF) passedOn(from string) {
s.t.Helper() s.t.Helper()
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: s.allowed(from), RequestID: s.allowed(from),
Status: http.StatusOK, Status: http.StatusOK,
RequestBytes: bodyBytes, RequestBytes: bodyBytes,
@@ -182,7 +182,7 @@ func (s *serveWAF) history() ratelimit.History {
func (s *serveWAF) metrics() string { func (s *serveWAF) metrics() string {
s.t.Helper() s.t.Helper()
req := newRequest(s.t, http.MethodGet, s.addr, proxy.MetricsPath, http.NoBody) req := newRequest(s.t, http.MethodGet, s.addr, servewaf.MetricsPath, http.NoBody)
req.Header.Set("Authorization", bearer) req.Header.Set("Authorization", bearer)
got := do(s.t, req) got := do(s.t, req)
@@ -196,7 +196,7 @@ func wantDisposition(t *testing.T, got answer, status int, disposition string) {
t.Helper() t.Helper()
wantStatus(t, got, status) wantStatus(t, got, status)
if answered := got.header.Get(proxy.DispositionHeader); answered != disposition { if answered := got.header.Get(servewaf.DispositionHeader); answered != disposition {
t.Errorf("answered with disposition %q, want %q", answered, disposition) t.Errorf("answered with disposition %q, want %q", answered, disposition)
} }
} }
@@ -248,10 +248,10 @@ func TestQuestionAllowedIsAnsweredAndLogged(t *testing.T) {
req := s.question(siteA, client, "/page?q=1&token=visible") req := s.question(siteA, client, "/page?q=1&token=visible")
req.Header.Set(requestIDHeader, "the-id") req.Header.Set(requestIDHeader, "the-id")
req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set(proxy.ContentLengthHeader, "1234") req.Header.Set(servewaf.ContentLengthHeader, "1234")
got := do(t, req) got := do(t, req)
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
wantBody(t, got, "") wantBody(t, got, "")
if id := got.header.Get(requestIDHeader); id != "" { if id := got.header.Get(requestIDHeader); id != "" {
@@ -278,7 +278,7 @@ func TestIPv6VisitorIsCountedInItsGroup(t *testing.T) {
s.allowed("2001:db8::1") s.allowed("2001:db8::1")
got := s.ask("2001:db8::2", "/") got := s.ask("2001:db8::2", "/")
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
} }
func TestIPv4VisitorInIPv6FormIsTheIPv4Client(t *testing.T) { func TestIPv4VisitorInIPv6FormIsTheIPv4Client(t *testing.T) {
@@ -287,9 +287,9 @@ func TestIPv4VisitorInIPv6FormIsTheIPv4Client(t *testing.T) {
s := startServeWAF(t, "", map[string]string{denyNets: client}) s := startServeWAF(t, "", map[string]string{denyNets: client})
req := s.question(siteA, "::ffff:"+client, "/") req := s.question(siteA, "::ffff:"+client, "/")
req.Header.Set(proxy.PeerHeader, "::ffff:"+rproxyPeer) req.Header.Set(servewaf.PeerHeader, "::ffff:"+rproxyPeer)
wantDisposition(t, do(t, req), http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, do(t, req), http.StatusForbidden, servewaf.DispositionDeny)
line := s.out.requestLine(t) line := s.out.requestLine(t)
if line.ClientIP != client || line.PeerIP != rproxyPeer { if line.ClientIP != client || line.PeerIP != rproxyPeer {
@@ -305,13 +305,13 @@ func TestMalformedQuestionGets400AndAWarningAndNoLine(t *testing.T) {
change func(*http.Request) change func(*http.Request)
}{ }{
{"client not an address", func(req *http.Request) { {"client not an address", func(req *http.Request) {
req.Header.Set(proxy.ClientHeader, "not an address") req.Header.Set(servewaf.ClientHeader, "not an address")
}}, }},
{"client empty", func(req *http.Request) { {"client empty", func(req *http.Request) {
req.Header.Set(proxy.ClientHeader, "") req.Header.Set(servewaf.ClientHeader, "")
}}, }},
{"client address with a zone", func(req *http.Request) { {"client address with a zone", func(req *http.Request) {
req.Header.Set(proxy.ClientHeader, "2001:db8::1%eth0") req.Header.Set(servewaf.ClientHeader, "2001:db8::1%eth0")
}}, }},
{"id absent", func(req *http.Request) { {"id absent", func(req *http.Request) {
req.Header.Del(requestIDHeader) req.Header.Del(requestIDHeader)
@@ -353,7 +353,7 @@ func TestLongestValidIDIsTaken(t *testing.T) {
req := s.question(siteA, client, "/") req := s.question(siteA, client, "/")
req.Header.Set(requestIDHeader, strings.Repeat("x", 128)) req.Header.Set(requestIDHeader, strings.Repeat("x", 128))
wantDisposition(t, do(t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, req), http.StatusOK, servewaf.DispositionAllow)
} }
func TestQuestionWithABodyOver64KiBGets400(t *testing.T) { func TestQuestionWithABodyOver64KiBGets400(t *testing.T) {
@@ -361,15 +361,15 @@ func TestQuestionWithABodyOver64KiBGets400(t *testing.T) {
s := startServeWAF(t, "", nil) s := startServeWAF(t, "", nil)
over := strings.NewReader(strings.Repeat("x", proxy.QuestionBodyMaxBytes+1)) over := strings.NewReader(strings.Repeat("x", servewaf.QuestionBodyMaxBytes+1))
got := do(t, s.questionWith(http.MethodPost, siteA, client, "/", over)) got := do(t, s.questionWith(http.MethodPost, siteA, client, "/", over))
wantDisposition(t, got, http.StatusBadRequest, "") wantDisposition(t, got, http.StatusBadRequest, "")
wantProcessMessages(t, s, "a question is refused", 1) wantProcessMessages(t, s, "a question is refused", 1)
wantNoRequestLines(t, s) wantNoRequestLines(t, s)
whole := strings.NewReader(strings.Repeat("x", proxy.QuestionBodyMaxBytes)) whole := strings.NewReader(strings.Repeat("x", servewaf.QuestionBodyMaxBytes))
got = do(t, s.questionWith(http.MethodPost, siteA, client, "/", whole)) got = do(t, s.questionWith(http.MethodPost, siteA, client, "/", whole))
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
} }
func TestQuestionWithAHeldIDIsRefused(t *testing.T) { func TestQuestionWithAHeldIDIsRefused(t *testing.T) {
@@ -385,15 +385,15 @@ func TestQuestionWithAHeldIDIsRefused(t *testing.T) {
wantProcessMessages(t, s, "a question is refused", 1) wantProcessMessages(t, s, "a question is refused", 1)
// A denied request's id is held as well. // A denied request's id is held as well.
denied := s.question(siteA, otherClient, proxy.HealthPath) denied := s.question(siteA, otherClient, servewaf.HealthPath)
wantDisposition(t, do(t, denied), http.StatusNotFound, proxy.DispositionDeny) wantDisposition(t, do(t, denied), http.StatusNotFound, servewaf.DispositionDeny)
again = s.question(siteA, client, "/") again = s.question(siteA, client, "/")
again.Header.Set(requestIDHeader, denied.Header.Get(requestIDHeader)) again.Header.Set(requestIDHeader, denied.Header.Get(requestIDHeader))
wantDisposition(t, do(t, again), http.StatusBadRequest, "") wantDisposition(t, do(t, again), http.StatusBadRequest, "")
// The report ends the holding, and the refused questions changed nothing. // The report ends the holding, and the refused questions changed nothing.
got := s.report(proxy.Report{RequestID: id, ResponseBytes: answerBytes}) got := s.report(servewaf.Report{RequestID: id, ResponseBytes: answerBytes})
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
if history := s.history(); history.ResponseBytes != answerBytes { if history := s.history(); history.ResponseBytes != answerBytes {
@@ -402,7 +402,7 @@ func TestQuestionWithAHeldIDIsRefused(t *testing.T) {
again = s.question(siteA, client, "/") again = s.question(siteA, client, "/")
again.Header.Set(requestIDHeader, id) again.Header.Set(requestIDHeader, id)
wantDisposition(t, do(t, again), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, again), http.StatusOK, servewaf.DispositionAllow)
} }
func TestReportNeverReachesAQuestionStillBeingHandled(t *testing.T) { func TestReportNeverReachesAQuestionStillBeingHandled(t *testing.T) {
@@ -436,7 +436,7 @@ func TestReportNeverReachesAQuestionStillBeingHandled(t *testing.T) {
// A report for a question not yet answered is ignored, and that question's // A report for a question not yet answered is ignored, and that question's
// id is taken. // id is taken.
got := s.report(proxy.Report{RequestID: id, ResponseBytes: 1000}) got := s.report(servewaf.Report{RequestID: id, ResponseBytes: 1000})
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
// This would wait on the same lookup were the id not taken. // This would wait on the same lookup were the id not taken.
@@ -451,7 +451,7 @@ func TestReportNeverReachesAQuestionStillBeingHandled(t *testing.T) {
select { select {
case got := <-answered: case got := <-answered:
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
case <-time.After(waitLimit): case <-time.After(waitLimit):
t.Fatal("the question got no answer") t.Fatal("the question got no answer")
} }
@@ -466,7 +466,7 @@ func TestReportNeverReachesAQuestionStillBeingHandled(t *testing.T) {
} }
// The true report comes after, and is the one that counts. // The true report comes after, and is the one that counts.
got = s.report(proxy.Report{RequestID: id, ResponseBytes: answerBytes}) got = s.report(servewaf.Report{RequestID: id, ResponseBytes: answerBytes})
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
if history := s.history(); history.ResponseBytes != answerBytes { if history := s.history(); history.ResponseBytes != answerBytes {
@@ -484,13 +484,13 @@ func TestPeerAbsentOrUnreadableIsAnsweredAndLoggedInvalid(t *testing.T) {
s := startServeWAF(t, "", nil) s := startServeWAF(t, "", nil)
req := s.question(siteA, client, "/") req := s.question(siteA, client, "/")
req.Header.Del(proxy.PeerHeader) req.Header.Del(servewaf.PeerHeader)
if peer != "" { if peer != "" {
req.Header.Set(proxy.PeerHeader, peer) req.Header.Set(servewaf.PeerHeader, peer)
} }
wantDisposition(t, do(t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, req), http.StatusOK, servewaf.DispositionAllow)
if line := s.out.requestLine(t); line.PeerIP != "invalid IP" { if line := s.out.requestLine(t); line.PeerIP != "invalid IP" {
t.Errorf("log line has peer_ip %q, want invalid IP", line.PeerIP) t.Errorf("log line has peer_ip %q, want invalid IP", line.PeerIP)
@@ -511,8 +511,8 @@ func TestContentLengthUnreadableOrNegativeCountsAsAbsent(t *testing.T) {
}) })
req := s.question(siteA, client, "/") req := s.question(siteA, client, "/")
req.Header.Set(proxy.ContentLengthHeader, announced) req.Header.Set(servewaf.ContentLengthHeader, announced)
wantDisposition(t, do(t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, req), http.StatusOK, servewaf.DispositionAllow)
if line := s.out.requestLine(t); line.ContentLength != 0 { if line := s.out.requestLine(t); line.ContentLength != 0 {
t.Errorf("log line has content_length %d, want none", line.ContentLength) t.Errorf("log line has content_length %d, want none", line.ContentLength)
@@ -527,13 +527,13 @@ func TestHeaderSentTwiceUsesItsFirstValue(t *testing.T) {
s := startServeWAF(t, "", nil) s := startServeWAF(t, "", nil)
req := s.question(siteA, client, "/") req := s.question(siteA, client, "/")
req.Header.Add(proxy.ClientHeader, otherClient) req.Header.Add(servewaf.ClientHeader, otherClient)
req.Header.Add(proxy.PeerHeader, "172.18.0.3") req.Header.Add(servewaf.PeerHeader, "172.18.0.3")
req.Header.Set(requestIDHeader, "first") req.Header.Set(requestIDHeader, "first")
req.Header.Add(requestIDHeader, "second") req.Header.Add(requestIDHeader, "second")
req.Header.Set(proxy.ContentLengthHeader, "1234") req.Header.Set(servewaf.ContentLengthHeader, "1234")
req.Header.Add(proxy.ContentLengthHeader, "5678") req.Header.Add(servewaf.ContentLengthHeader, "5678")
wantDisposition(t, do(t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, req), http.StatusOK, servewaf.DispositionAllow)
line := s.out.requestLine(t) line := s.out.requestLine(t)
if line.ClientIP != client || line.PeerIP != rproxyPeer || if line.ClientIP != client || line.PeerIP != rproxyPeer ||
@@ -562,7 +562,7 @@ func sendQuestionWithPartOfItsBody(t *testing.T, conn net.Conn) {
_, err := fmt.Fprintf(conn, _, err := fmt.Fprintf(conn,
"POST / HTTP/1.1\r\nHost: %s\r\n%s: %s\r\n%s: an-id\r\n"+ "POST / HTTP/1.1\r\nHost: %s\r\n%s: %s\r\n%s: an-id\r\n"+
"Content-Length: 100\r\n\r\npart", "Content-Length: 100\r\n\r\npart",
siteA, proxy.ClientHeader, client, requestIDHeader) siteA, servewaf.ClientHeader, client, requestIDHeader)
if err != nil { if err != nil {
t.Fatalf("send the question: %v", err) t.Fatalf("send the question: %v", err)
} }
@@ -592,7 +592,7 @@ func TestQuestionWhoseBodyStopsArrivingGetsNoAnswer(t *testing.T) {
t.Parallel() t.Parallel()
s := startServeWAFWith(t, "", map[string]string{metricsToken: token}, s := startServeWAFWith(t, "", map[string]string{metricsToken: token},
func(server *proxy.Server) { server.ReadTimeout = readTimeout }) func(server *servewaf.Server) { server.ReadTimeout = readTimeout })
conn := dial(t, s.addr) conn := dial(t, s.addr)
sendQuestionWithPartOfItsBody(t, conn) sendQuestionWithPartOfItsBody(t, conn)
@@ -615,7 +615,7 @@ func TestQuestionWhoseBodyStopsArrivingGetsNoAnswer(t *testing.T) {
// The id was not taken. // The id was not taken.
again := s.question(siteA, client, "/") again := s.question(siteA, client, "/")
again.Header.Set(requestIDHeader, "an-id") again.Header.Set(requestIDHeader, "an-id")
wantDisposition(t, do(t, again), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, again), http.StatusOK, servewaf.DispositionAllow)
}) })
} }
} }
@@ -632,7 +632,7 @@ func TestQuestionDeniedGetsTheBanResponse(t *testing.T) {
}) })
got := s.ask(client, "/") got := s.ask(client, "/")
wantDisposition(t, got, status, proxy.DispositionDeny) wantDisposition(t, got, status, servewaf.DispositionDeny)
wantBody(t, got, http.StatusText(status)+"\n") wantBody(t, got, http.StatusText(status)+"\n")
wantLine(t, s.out.requestLine(t), status, requestlog.ActionDenied) wantLine(t, s.out.requestLine(t), status, requestlog.ActionDenied)
}) })
@@ -647,7 +647,7 @@ func TestBanResponseCloseIsAnAnswerOfItsOwn(t *testing.T) {
}) })
got := s.ask(client, "/") got := s.ask(client, "/")
wantDisposition(t, got, http.StatusOK, proxy.DispositionClose) wantDisposition(t, got, http.StatusOK, servewaf.DispositionClose)
wantBody(t, got, "") wantBody(t, got, "")
// The visitor is sent nothing. // The visitor is sent nothing.
wantLine(t, s.out.requestLine(t), 0, requestlog.ActionDenied) wantLine(t, s.out.requestLine(t), 0, requestlog.ActionDenied)
@@ -664,12 +664,12 @@ func TestBlockRuleIsDeniedWith403AndABanRuleWithTheBanResponse(t *testing.T) {
}) })
got := s.ask(client, "/blocked") got := s.ask(client, "/blocked")
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
wantLine(t, s.out.requestLine(t), http.StatusForbidden, wantLine(t, s.out.requestLine(t), http.StatusForbidden,
requestlog.ActionRuleBlocked) requestlog.ActionRuleBlocked)
got = s.ask(client, "/.env") got = s.ask(client, "/.env")
wantDisposition(t, got, http.StatusTooManyRequests, proxy.DispositionDeny) wantDisposition(t, got, http.StatusTooManyRequests, servewaf.DispositionDeny)
wantLine(t, s.out.requestLines(t, 2)[1], http.StatusTooManyRequests, wantLine(t, s.out.requestLines(t, 2)[1], http.StatusTooManyRequests,
requestlog.ActionBanned) requestlog.ActionBanned)
} }
@@ -681,15 +681,17 @@ func TestQuestionUnderTheOwnPrefixIsDeniedWith404AfterTheChecks(t *testing.T) {
metricsToken: token, denyNets: otherClient, metricsToken: token, denyNets: otherClient,
}) })
for _, path := range []string{proxy.HealthPath, proxy.MetricsPath, proxy.ReportPath} { for _, path := range []string{
servewaf.HealthPath, servewaf.MetricsPath, servewaf.ReportPath,
} {
got := s.ask(client, path) got := s.ask(client, path)
wantDisposition(t, got, http.StatusNotFound, proxy.DispositionDeny) wantDisposition(t, got, http.StatusNotFound, servewaf.DispositionDeny)
} }
wantLine(t, s.out.requestLine(t), http.StatusNotFound, requestlog.ActionAdmin) wantLine(t, s.out.requestLine(t), http.StatusNotFound, requestlog.ActionAdmin)
got := s.ask(otherClient, proxy.MetricsPath) got := s.ask(otherClient, servewaf.MetricsPath)
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
} }
func TestObserveModeAllowsAndLogsWhatEnforceModeWouldHaveDone(t *testing.T) { func TestObserveModeAllowsAndLogsWhatEnforceModeWouldHaveDone(t *testing.T) {
@@ -698,7 +700,7 @@ func TestObserveModeAllowsAndLogsWhatEnforceModeWouldHaveDone(t *testing.T) {
s := startServeWAF(t, "", map[string]string{mode: observe, denyNets: client}) s := startServeWAF(t, "", map[string]string{mode: observe, denyNets: client})
got := s.ask(client, "/") got := s.ask(client, "/")
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
line := s.out.requestLine(t) line := s.out.requestLine(t)
wantLine(t, line, http.StatusOK, requestlog.ActionForward) wantLine(t, line, http.StatusOK, requestlog.ActionForward)
@@ -714,7 +716,7 @@ func TestObserveModeRefusalAnsweredAllowCountsForTheErrorBurst(t *testing.T) {
for range 2 { for range 2 {
got := s.ask(client, "/blocked") got := s.ask(client, "/blocked")
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
} }
lines := s.out.requestLines(t, 2) lines := s.out.requestLines(t, 2)
@@ -733,7 +735,7 @@ func TestDeniedQuestionCountsForTheErrorBurst(t *testing.T) {
for range 2 { for range 2 {
got := s.ask(client, "/blocked") got := s.ask(client, "/blocked")
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
} }
lines := s.out.requestLines(t, 2) lines := s.out.requestLines(t, 2)
@@ -762,7 +764,7 @@ func TestAllowCarriesTheLookupHeadersWhileTheSettingIsSet(t *testing.T) {
}) })
got := s.ask(fromDE, "/") got := s.ask(fromDE, "/")
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
asn, country := got.header.Get("X-Client-Asn"), got.header.Get("X-Client-Country") asn, country := got.header.Get("X-Client-Asn"), got.header.Get("X-Client-Country")
if asn != tc.wantASN || country != tc.wantCountry { if asn != tc.wantASN || country != tc.wantCountry {
@@ -787,7 +789,7 @@ func TestReportCountsTowardAByteLimitWhichThenBans(t *testing.T) {
} }
// 100 bytes are over the limit of 99. // 100 bytes are over the limit of 99.
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: id, Status: http.StatusBadGateway, RequestID: id, Status: http.StatusBadGateway,
RequestBytes: bodyBytes, ResponseBytes: answerBytes, RequestBytes: bodyBytes, ResponseBytes: answerBytes,
}) })
@@ -811,7 +813,7 @@ func TestReportCountsTowardAByteLimitWhichThenBans(t *testing.T) {
} }
got = s.ask(client, "/") got = s.ask(client, "/")
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
} }
func TestReportCountsAtThePercentageItsQuestionWasGiven(t *testing.T) { func TestReportCountsAtThePercentageItsQuestionWasGiven(t *testing.T) {
@@ -825,10 +827,10 @@ func TestReportCountsAtThePercentageItsQuestionWasGiven(t *testing.T) {
}) })
s.passedOn(fromKP) s.passedOn(fromKP)
wantDisposition(t, s.ask(fromKP, "/"), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, s.ask(fromKP, "/"), http.StatusOK, servewaf.DispositionAllow)
s.passedOn(fromDE) s.passedOn(fromDE)
wantDisposition(t, s.ask(fromDE, "/"), http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, s.ask(fromDE, "/"), http.StatusForbidden, servewaf.DispositionDeny)
} }
func TestReportAddsTheRequestToItsClientsHistoryAndTheAnomalyCounters(t *testing.T) { func TestReportAddsTheRequestToItsClientsHistoryAndTheAnomalyCounters(t *testing.T) {
@@ -846,7 +848,7 @@ func TestReportAddsTheRequestToItsClientsHistoryAndTheAnomalyCounters(t *testing
t.Errorf("anomaly counters %v before the report, want none", counters) t.Errorf("anomaly counters %v before the report, want none", counters)
} }
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: id, Status: http.StatusBadGateway, RequestID: id, Status: http.StatusBadGateway,
RequestBytes: bodyBytes, ResponseBytes: answerBytes, RequestBytes: bodyBytes, ResponseBytes: answerBytes,
}) })
@@ -874,7 +876,7 @@ func TestDeniedRequestIsFinishedAtItsDecision(t *testing.T) {
s.allowed(client) s.allowed(client)
denied := s.question(siteA, client, "/") denied := s.question(siteA, client, "/")
wantDisposition(t, do(t, denied), http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, do(t, denied), http.StatusForbidden, servewaf.DispositionDeny)
counters := s.server.Anomalies.Snapshot() counters := s.server.Anomalies.Snapshot()
if len(counters) != 1 || counters[0].Minute.Current != 1 { if len(counters) != 1 || counters[0].Minute.Current != 1 {
@@ -891,7 +893,7 @@ func TestDeniedRequestIsFinishedAtItsDecision(t *testing.T) {
wantMetric(t, s.metrics(), series, 1) wantMetric(t, s.metrics(), series, 1)
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: denied.Header.Get(requestIDHeader), Status: http.StatusForbidden, RequestID: denied.Header.Get(requestIDHeader), Status: http.StatusForbidden,
ResponseBytes: 1000, DurationTotal: 2, ResponseBytes: 1000, DurationTotal: 2,
}) })
@@ -924,7 +926,7 @@ func TestMetricsCountAnAllowedRequestFromItsReport(t *testing.T) {
wantMetric(t, metrics, `smallwebwaf_requests_in_flight{instance="`+alertInstance+`"}`, wantMetric(t, metrics, `smallwebwaf_requests_in_flight{instance="`+alertInstance+`"}`,
1) // the scrape itself 1) // the scrape itself
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: id, Status: http.StatusBadGateway, RequestID: id, Status: http.StatusBadGateway,
RequestBytes: bodyBytes, ResponseBytes: answerBytes, RequestBytes: bodyBytes, ResponseBytes: answerBytes,
DurationTotal: 5, DurationUpstreamTotal: 4, DurationTotal: 5, DurationUpstreamTotal: 4,
@@ -942,7 +944,7 @@ func TestMetricsCountAnAllowedRequestFromItsReport(t *testing.T) {
// check needs a server that has not been scraped yet. // check needs a server that has not been scraped yet.
unscraped := startServeWAF(t, "", map[string]string{metricsToken: token}) unscraped := startServeWAF(t, "", map[string]string{metricsToken: token})
got = unscraped.report(proxy.Report{ got = unscraped.report(servewaf.Report{
RequestID: unscraped.allowed(client), Status: http.StatusBadGateway, DurationTotal: 5, RequestID: unscraped.allowed(client), Status: http.StatusBadGateway, DurationTotal: 5,
}) })
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
@@ -955,7 +957,7 @@ func TestReportForARequestNotHeldIsIgnored(t *testing.T) {
s := startServeWAF(t, "", map[string]string{bytesLimitPerMinute: byteLimit}) s := startServeWAF(t, "", map[string]string{bytesLimitPerMinute: byteLimit})
got := s.report(proxy.Report{RequestID: "no-such-request", ResponseBytes: 1000}) got := s.report(servewaf.Report{RequestID: "no-such-request", ResponseBytes: 1000})
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
if clients := s.server.Limiter.Snapshot(); len(clients) != 0 { if clients := s.server.Limiter.Snapshot(); len(clients) != 0 {
@@ -972,7 +974,7 @@ func TestReportForARequestNotHeldIsIgnored(t *testing.T) {
// A second report of the same request does not count it twice. // A second report of the same request does not count it twice.
id := s.allowed(client) id := s.allowed(client)
for range 2 { for range 2 {
got = s.report(proxy.Report{RequestID: id, ResponseBytes: answerBytes}) got = s.report(servewaf.Report{RequestID: id, ResponseBytes: answerBytes})
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
} }
@@ -1160,7 +1162,7 @@ func TestBytesAtTheirMostEachWayBanRatherThanOverflow(t *testing.T) {
s := startServeWAF(t, "", map[string]string{bytesLimitPerMinute: byteLimit}) s := startServeWAF(t, "", map[string]string{bytesLimitPerMinute: byteLimit})
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: s.allowed(client), Status: http.StatusOK, RequestID: s.allowed(client), Status: http.StatusOK,
RequestBytes: reportedBytesLimit, ResponseBytes: reportedBytesLimit, RequestBytes: reportedBytesLimit, ResponseBytes: reportedBytesLimit,
}) })
@@ -1245,9 +1247,9 @@ func TestReportWritesTheLastLineWithTheDecisionsAndTheReportsFields(t *testing.T
req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Proto", "https")
id := req.Header.Get(requestIDHeader) id := req.Header.Get(requestIDHeader)
wantDisposition(t, do(t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, req), http.StatusOK, servewaf.DispositionAllow)
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: id, Status: http.StatusCreated, RequestID: id, Status: http.StatusCreated,
RequestBytes: bodyBytes, ResponseBytes: answerBytes, RequestBytes: bodyBytes, ResponseBytes: answerBytes,
DurationTotal: 12.5, DurationUpstreamTotal: 10.25, DurationTotal: 12.5, DurationUpstreamTotal: 10.25,
@@ -1294,7 +1296,7 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
"close", map[string]string{denyNets: client, banResponse: banResponseClose}, "/", "close", map[string]string{denyNets: client, banResponse: banResponseClose}, "/",
0, requestlog.ActionDenied, 0, requestlog.ActionDenied,
}, },
{"own prefix", nil, proxy.HealthPath, http.StatusNotFound, requestlog.ActionAdmin}, {"own prefix", nil, servewaf.HealthPath, http.StatusNotFound, requestlog.ActionAdmin},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1305,7 +1307,7 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
id := req.Header.Get(requestIDHeader) id := req.Header.Get(requestIDHeader)
do(t, req) do(t, req)
got := s.report(proxy.Report{ got := s.report(servewaf.Report{
RequestID: id, Status: tc.status, ResponseBytes: 8, DurationTotal: 3, RequestID: id, Status: tc.status, ResponseBytes: 8, DurationTotal: 3,
}) })
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
@@ -1341,18 +1343,18 @@ func TestOldestAnsweredRequestIsDroppedPastTheBound(t *testing.T) {
t.Fatalf("question: %v", err) t.Fatalf("question: %v", err)
} }
wantDisposition(t, readAnswer(res), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, readAnswer(res), http.StatusOK, servewaf.DispositionAllow)
} }
// The table is full again with next and those, without oldest. // The table is full again with next and those, without oldest.
got := s.report(proxy.Report{RequestID: oldest, ResponseBytes: answerBytes}) got := s.report(servewaf.Report{RequestID: oldest, ResponseBytes: answerBytes})
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
if counted := s.history().Requests; counted != 0 { if counted := s.history().Requests; counted != 0 {
t.Errorf("history holds %d requests, want the dropped one not counted", counted) t.Errorf("history holds %d requests, want the dropped one not counted", counted)
} }
got = s.report(proxy.Report{RequestID: next, ResponseBytes: answerBytes}) got = s.report(servewaf.Report{RequestID: next, ResponseBytes: answerBytes})
wantStatus(t, got, http.StatusNoContent) wantStatus(t, got, http.StatusNoContent)
if counted := s.history().Requests; counted != 1 { if counted := s.history().Requests; counted != 1 {
@@ -1366,10 +1368,10 @@ func TestBanMadeThroughOneHostRefusesTheClientOnAnother(t *testing.T) {
s := startServeWAF(t, "", map[string]string{rulesDir: writeRules(t, testRules)}) s := startServeWAF(t, "", map[string]string{rulesDir: writeRules(t, testRules)})
got := do(t, s.question(siteA, client, "/.env")) got := do(t, s.question(siteA, client, "/.env"))
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
got = do(t, s.question(siteB, client, "/")) got = do(t, s.question(siteB, client, "/"))
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
lines := s.out.requestLines(t, 2) lines := s.out.requestLines(t, 2)
if lines[0].Host != siteA || lines[1].Host != siteB { if lines[0].Host != siteA || lines[1].Host != siteB {
@@ -1395,7 +1397,7 @@ func TestRequestsOnEverySiteCountTowardOneSetOfLimits(t *testing.T) {
s.allowed(client) s.allowed(client)
got := do(t, s.question(siteB, client, "/")) got := do(t, s.question(siteB, client, "/"))
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
} }
func TestOwnEndpointsAnswerOnlyWithoutTheClientHeader(t *testing.T) { func TestOwnEndpointsAnswerOnlyWithoutTheClientHeader(t *testing.T) {
@@ -1410,13 +1412,13 @@ func TestOwnEndpointsAnswerOnlyWithoutTheClientHeader(t *testing.T) {
path, authorization string path, authorization string
want int want int
}{ }{
{proxy.HealthPath, "", http.StatusOK}, {servewaf.HealthPath, "", http.StatusOK},
{proxy.MetricsPath, bearer, http.StatusOK}, {servewaf.MetricsPath, bearer, http.StatusOK},
{proxy.MetricsPath, "", http.StatusUnauthorized}, {servewaf.MetricsPath, "", http.StatusUnauthorized},
{proxy.BansPath, adminBearer, http.StatusOK}, {servewaf.BansPath, adminBearer, http.StatusOK},
{proxy.BansPath, "", http.StatusUnauthorized}, {servewaf.BansPath, "", http.StatusUnauthorized},
{proxy.ClientsPath + client, adminBearer, http.StatusOK}, {servewaf.ClientsPath + client, adminBearer, http.StatusOK},
{proxy.ReportPath, "", http.StatusNotFound}, {servewaf.ReportPath, "", http.StatusNotFound},
{"/_smallwebwaf/other", "", http.StatusNotFound}, {"/_smallwebwaf/other", "", http.StatusNotFound},
{"/", "", http.StatusNotFound}, {"/", "", http.StatusNotFound},
} { } {
@@ -1429,9 +1431,9 @@ func TestOwnEndpointsAnswerOnlyWithoutTheClientHeader(t *testing.T) {
req.Header.Set("Authorization", tc.authorization) req.Header.Set("Authorization", tc.authorization)
if strings.HasPrefix(tc.path, "/_smallwebwaf/") { if strings.HasPrefix(tc.path, "/_smallwebwaf/") {
wantDisposition(t, do(t, req), http.StatusNotFound, proxy.DispositionDeny) wantDisposition(t, do(t, req), http.StatusNotFound, servewaf.DispositionDeny)
} else { } else {
wantDisposition(t, do(t, req), http.StatusOK, proxy.DispositionAllow) wantDisposition(t, do(t, req), http.StatusOK, servewaf.DispositionAllow)
} }
} }
} }
@@ -1441,10 +1443,11 @@ func TestOwnEndpointsLinesDoNotSayWhichOfTheTwoTheyAre(t *testing.T) {
s := startServeWAF(t, "", nil) s := startServeWAF(t, "", nil)
wantStatus(t, get(t, s.addr, proxy.HealthPath), http.StatusOK) wantStatus(t, get(t, s.addr, servewaf.HealthPath), http.StatusOK)
line := s.out.requestLine(t) line := s.out.requestLine(t)
if _, there := line.fields["servewaf_line"]; there || line.Path != proxy.HealthPath { if _, there := line.fields["servewaf_line"]; there ||
line.Path != servewaf.HealthPath {
t.Errorf("the health check's log line is %+v", line.fields) t.Errorf("the health check's log line is %+v", line.fields)
} }
} }
@@ -1456,12 +1459,12 @@ func TestALargeBodyAnnouncedAndLongHeadersAreNoReasonToRefuse(t *testing.T) {
req := s.questionWith(http.MethodPost, siteA, client, "/", req := s.questionWith(http.MethodPost, siteA, client, "/",
strings.NewReader("a body")) strings.NewReader("a body"))
req.Header.Set(proxy.ContentLengthHeader, "1073741824") req.Header.Set(servewaf.ContentLengthHeader, "1073741824")
req.Header.Set(forwardedFor, otherClient) req.Header.Set(forwardedFor, otherClient)
req.Header.Set("X-Long", strings.Repeat("x", 40<<10)) req.Header.Set("X-Long", strings.Repeat("x", 40<<10))
got := do(t, req) got := do(t, req)
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
line := s.out.requestLine(t) line := s.out.requestLine(t)
if line.ClientIP != client || line.ContentLength != 1<<30 { if line.ClientIP != client || line.ContentLength != 1<<30 {
@@ -1529,7 +1532,7 @@ carried header:Content-Length block ^6$
strings.NewReader("a body")) strings.NewReader("a body"))
if tc.announced != "" { if tc.announced != "" {
req.Header.Set(proxy.ContentLengthHeader, tc.announced) req.Header.Set(servewaf.ContentLengthHeader, tc.announced)
} }
wantStatus(t, do(t, req), tc.want) wantStatus(t, do(t, req), tc.want)
@@ -1547,19 +1550,19 @@ func TestCoreRuleSetReadsTheBodyAQuestionCarries(t *testing.T) {
req := s.questionWith(http.MethodPost, siteA, client, "/", req := s.questionWith(http.MethodPost, siteA, client, "/",
strings.NewReader(body)) strings.NewReader(body))
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
req.Header.Set(proxy.ContentLengthHeader, "100000") req.Header.Set(servewaf.ContentLengthHeader, "100000")
return do(t, req) return do(t, req)
} }
got := ask(formData, "id=1'%20OR%20'1'='1") got := ask(formData, "id=1'%20OR%20'1'='1")
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
// The first 64 KiB of a longer JSON body cannot be parsed, which is no // The first 64 KiB of a longer JSON body cannot be parsed, which is no
// attack, so the Core Rule Set does not read it. // attack, so the Core Rule Set does not read it.
cut := `{"text":"` + strings.Repeat("x", proxy.QuestionBodyMaxBytes) cut := `{"text":"` + strings.Repeat("x", servewaf.QuestionBodyMaxBytes)
got = ask("application/json", cut[:proxy.QuestionBodyMaxBytes]) got = ask("application/json", cut[:servewaf.QuestionBodyMaxBytes])
wantDisposition(t, got, http.StatusOK, proxy.DispositionAllow) wantDisposition(t, got, http.StatusOK, servewaf.DispositionAllow)
} }
func TestCoreRuleSetAlertNamesTheSite(t *testing.T) { func TestCoreRuleSetAlertNamesTheSite(t *testing.T) {
@@ -1568,7 +1571,7 @@ func TestCoreRuleSetAlertNamesTheSite(t *testing.T) {
s := startServeWAF(t, "", map[string]string{wafMode: block}) s := startServeWAF(t, "", map[string]string{wafMode: block})
got := s.ask(client, sqlInjection) got := s.ask(client, sqlInjection)
wantDisposition(t, got, http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, got, http.StatusForbidden, servewaf.DispositionDeny)
waiting := s.queue.Snapshot().Waiting[alerts.DestinationWebhook] waiting := s.queue.Snapshot().Waiting[alerts.DestinationWebhook]
if len(waiting) != 1 || waiting[0].Event != alerts.EventWAFBlock || if len(waiting) != 1 || waiting[0].Event != alerts.EventWAFBlock ||
@@ -1583,7 +1586,7 @@ func TestReputationHitAlertNamesTheSite(t *testing.T) {
s := startServeWAF(t, "", map[string]string{blocklistURLs: dropURL}) s := startServeWAF(t, "", map[string]string{blocklistURLs: dropURL})
loadLists(t, s.server, map[string][]string{dropURL: {client}}) loadLists(t, s.server, map[string][]string{dropURL: {client}})
wantDisposition(t, s.ask(client, "/"), http.StatusForbidden, proxy.DispositionDeny) wantDisposition(t, s.ask(client, "/"), http.StatusForbidden, servewaf.DispositionDeny)
waiting := s.queue.Snapshot().Waiting[alerts.DestinationWebhook] waiting := s.queue.Snapshot().Waiting[alerts.DestinationWebhook]
if len(waiting) != 1 || waiting[0].Event != alerts.EventReputationHit || if len(waiting) != 1 || waiting[0].Event != alerts.EventReputationHit ||
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
@@ -1,4 +1,4 @@
package proxy_test package servewaf_test
import ( import (
"net/http" "net/http"
+2 -2
View File
@@ -11,7 +11,7 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/proxy" "sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
// healthCheckTimeout bounds the whole health check. // healthCheckTimeout bounds the whole health check.
@@ -107,7 +107,7 @@ func checkRProxyAndApp(
// listenAddr, which the settings have checked has one. // listenAddr, which the settings have checked has one.
func askHealthEndpoint(ctx context.Context, listenAddr string) error { func askHealthEndpoint(ctx context.Context, listenAddr string) error {
_, port, _ := net.SplitHostPort(listenAddr) _, port, _ := net.SplitHostPort(listenAddr)
health := "http://" + net.JoinHostPort("127.0.0.1", port) + proxy.HealthPath health := "http://" + net.JoinHostPort("127.0.0.1", port) + servewaf.HealthPath
req, err := http.NewRequestWithContext(ctx, http.MethodGet, health, http.NoBody) req, err := http.NewRequestWithContext(ctx, http.MethodGet, health, http.NoBody)
if err != nil { if err != nil {
+10 -10
View File
@@ -19,7 +19,7 @@ import (
"testing" "testing"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/proxy" "sneak.berlin/go/smallwebwaf/internal/servewaf"
"sneak.berlin/go/smallwebwaf/internal/smallwebwaf" "sneak.berlin/go/smallwebwaf/internal/smallwebwaf"
) )
@@ -153,7 +153,7 @@ func TestABanThroughOneSiteRefusesTheClientOnTheOther(t *testing.T) {
// One request's three lines carry one id, rproxy's with servewaf's answer. // One request's three lines carry one id, rproxy's with servewaf's answer.
line := firstOut.line(t, "path", bannedProbe) line := firstOut.line(t, "path", bannedProbe)
if line["disposition"] != proxy.DispositionDeny || line["client_ip"] != placed { if line["disposition"] != servewaf.DispositionDeny || line["client_ip"] != placed {
t.Errorf("rproxy's line is %v", line) t.Errorf("rproxy's line is %v", line)
} }
@@ -225,7 +225,7 @@ func TestTheSiteAnswersWhileServeWAFIsStoppedAndStartedAgain(t *testing.T) {
wantStatus(t, site+"/after-stop", placed, http.StatusOK) wantStatus(t, site+"/after-stop", placed, http.StatusOK)
line := out.line(t, "path", "/after-stop") line := out.line(t, "path", "/after-stop")
if line["disposition"] != proxy.DispositionAllow || if line["disposition"] != servewaf.DispositionAllow ||
line["default_disposition"] != true { line["default_disposition"] != true {
t.Errorf("rproxy's line is %v", line) t.Errorf("rproxy's line is %v", line)
} }
@@ -318,7 +318,7 @@ func TestAChunkedBodyStaysABodyAtServeWAF(t *testing.T) {
const smuggledID = "smuggled-request" const smuggledID = "smuggled-request"
smuggled := "GET " + bannedProbe + " HTTP/1.1\r\nHost: site.example\r\n" + smuggled := "GET " + bannedProbe + " HTTP/1.1\r\nHost: site.example\r\n" +
proxy.ClientHeader + ": " + otherClient + "\r\n" + servewaf.ClientHeader + ": " + otherClient + "\r\n" +
"X-Request-ID: " + smuggledID + "\r\n\r\n" "X-Request-ID: " + smuggledID + "\r\n\r\n"
status := sendRaw(t, site, "POST /upload HTTP/1.1\r\nHost: site.example\r\n"+ status := sendRaw(t, site, "POST /upload HTTP/1.1\r\nHost: site.example\r\n"+
@@ -459,10 +459,10 @@ func TestRProxyAsksAServeWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) {
protocols := make(chan string, 10) protocols := make(chan string, 10)
backend := httptest.NewUnstartedServer(http.HandlerFunc( backend := httptest.NewUnstartedServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(proxy.ClientHeader) != "" { if r.Header.Get(servewaf.ClientHeader) != "" {
protocols <- r.Proto protocols <- r.Proto
w.Header().Set(proxy.DispositionHeader, proxy.DispositionDeny) w.Header().Set(servewaf.DispositionHeader, servewaf.DispositionDeny)
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
} }
})) }))
@@ -532,8 +532,8 @@ func TestRProxyStoppingWaitsForAReportBeingSent(t *testing.T) {
release := make(chan struct{}) release := make(chan struct{})
backend := httptest.NewServer(http.HandlerFunc( backend := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(proxy.ClientHeader) != "" { if r.Header.Get(servewaf.ClientHeader) != "" {
w.Header().Set(proxy.DispositionHeader, proxy.DispositionAllow) w.Header().Set(servewaf.DispositionHeader, servewaf.DispositionAllow)
return return
} }
@@ -640,7 +640,7 @@ func TestRProxyStoppingCutsOffARequestAfterFiveSeconds(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc( backend := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) { func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(proxy.DispositionHeader, proxy.DispositionAllow) w.Header().Set(servewaf.DispositionHeader, servewaf.DispositionAllow)
})) }))
t.Cleanup(backend.Close) t.Cleanup(backend.Close)
@@ -757,7 +757,7 @@ func TestMainRunsRProxy(t *testing.T) {
addr, cmd, out := startRProxyProcess(t, wafBackend+"=http://127.0.0.1:9") addr, cmd, out := startRProxyProcess(t, wafBackend+"=http://127.0.0.1:9")
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, req, err := http.NewRequestWithContext(t.Context(), http.MethodGet,
"http://"+addr+proxy.HealthPath, http.NoBody) "http://"+addr+servewaf.HealthPath, http.NoBody)
if err != nil { if err != nil {
t.Fatalf("new request: %v", err) t.Fatalf("new request: %v", err)
} }
+8 -8
View File
@@ -14,7 +14,7 @@ import (
"testing" "testing"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/proxy" "sneak.berlin/go/smallwebwaf/internal/servewaf"
"sneak.berlin/go/smallwebwaf/internal/smallwebwaf" "sneak.berlin/go/smallwebwaf/internal/smallwebwaf"
) )
@@ -67,7 +67,7 @@ func TestMainRunsServeWAF(t *testing.T) {
}) })
addr, _ := out.line(t, "msg", "starting")["address"].(string) addr, _ := out.line(t, "msg", "starting")["address"].(string)
ask(t, addr, "/", http.StatusOK, proxy.DispositionAllow) ask(t, addr, "/", http.StatusOK, servewaf.DispositionAllow)
err = cmd.Process.Signal(syscall.SIGTERM) err = cmd.Process.Signal(syscall.SIGTERM)
if err != nil { if err != nil {
@@ -132,7 +132,7 @@ func ask(t *testing.T, addr, path string, status int, disposition string) {
t.Fatalf("new request: %v", err) t.Fatalf("new request: %v", err)
} }
req.Header.Set(proxy.ClientHeader, placed) req.Header.Set(servewaf.ClientHeader, placed)
req.Header.Set("X-Request-ID", rand.Text()) req.Header.Set("X-Request-ID", rand.Text())
transport := &http.Transport{} transport := &http.Transport{}
@@ -147,9 +147,9 @@ func ask(t *testing.T, addr, path string, status int, disposition string) {
_ = res.Body.Close() _ = res.Body.Close()
if res.StatusCode != status || if res.StatusCode != status ||
res.Header.Get(proxy.DispositionHeader) != disposition { res.Header.Get(servewaf.DispositionHeader) != disposition {
t.Errorf("answered %d with disposition %q, want %d with %q", res.StatusCode, t.Errorf("answered %d with disposition %q, want %d with %q", res.StatusCode,
res.Header.Get(proxy.DispositionHeader), status, disposition) res.Header.Get(servewaf.DispositionHeader), status, disposition)
} }
} }
@@ -193,12 +193,12 @@ func TestServeWAFKeepsItsBansAcrossARestartAndNeedsNoApp(t *testing.T) {
} }
runServeWAF(t, env, func(addr string) { runServeWAF(t, env, func(addr string) {
ask(t, addr, "/", http.StatusOK, proxy.DispositionAllow) ask(t, addr, "/", http.StatusOK, servewaf.DispositionAllow)
// The default rule file bans a probe for this. // The default rule file bans a probe for this.
ask(t, addr, "/.env", http.StatusForbidden, proxy.DispositionDeny) ask(t, addr, "/.env", http.StatusForbidden, servewaf.DispositionDeny)
}) })
runServeWAF(t, env, func(addr string) { runServeWAF(t, env, func(addr string) {
ask(t, addr, "/", http.StatusForbidden, proxy.DispositionDeny) ask(t, addr, "/", http.StatusForbidden, servewaf.DispositionDeny)
}) })
} }
+5 -5
View File
@@ -23,11 +23,11 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/remotelog" "sneak.berlin/go/smallwebwaf/internal/remotelog"
"sneak.berlin/go/smallwebwaf/internal/reputation" "sneak.berlin/go/smallwebwaf/internal/reputation"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rules" "sneak.berlin/go/smallwebwaf/internal/rules"
"sneak.berlin/go/smallwebwaf/internal/servewaf"
"sneak.berlin/go/smallwebwaf/internal/state" "sneak.berlin/go/smallwebwaf/internal/state"
) )
@@ -190,7 +190,7 @@ func RunServeWAF(ctx context.Context, params Params) int {
func newServer( func newServer(
cfg *config.Config, stdout io.Writer, processLog *slog.Logger, cfg *config.Config, stdout io.Writer, processLog *slog.Logger,
now func() time.Time, ruleFiles *rules.Files, alertQueue *alerts.Queue, now func() time.Time, ruleFiles *rules.Files, alertQueue *alerts.Queue,
) (*proxy.Server, error) { ) (*servewaf.Server, error) {
var lookupFile *lookup.File var lookupFile *lookup.File
if cfg.LookupSource == "file" { if cfg.LookupSource == "file" {
@@ -207,7 +207,7 @@ func newServer(
} }
} }
server := proxy.NewServeWAF(proxy.Params{ server := servewaf.New(servewaf.Params{
Config: cfg, Config: cfg,
RequestLog: stdout, RequestLog: stdout,
ProcessLog: processLog, ProcessLog: processLog,
@@ -230,7 +230,7 @@ func newServer(
// loadStateFiles reads the state files into the parts of server and into // loadStateFiles reads the state files into the parts of server and into
// alertQueue, as state.Load does. // alertQueue, as state.Load does.
func loadStateFiles( func loadStateFiles(
cfg *config.Config, server *proxy.Server, alertQueue *alerts.Queue, cfg *config.Config, server *servewaf.Server, alertQueue *alerts.Queue,
now func() time.Time, processLog *slog.Logger, now func() time.Time, processLog *slog.Logger,
) (*state.Files, error) { ) (*state.Files, error) {
return state.Load(state.Params{ return state.Load(state.Params{
@@ -316,7 +316,7 @@ func startSending(
// shutdownTimeout to finish, and writes every state file, alerts.json with // shutdownTimeout to finish, and writes every state file, alerts.json with
// the alerts still waiting. // the alerts still waiting.
func serve( func serve(
ctx context.Context, server *proxy.Server, listener net.Listener, ctx context.Context, server *servewaf.Server, listener net.Listener,
files *state.Files, ruleFiles *rules.Files, alertQueue *alerts.Queue, files *state.Files, ruleFiles *rules.Files, alertQueue *alerts.Queue,
processLog *slog.Logger, processLog *slog.Logger,
) int { ) int {
+2 -2
View File
@@ -20,7 +20,7 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest" "sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
"sneak.berlin/go/smallwebwaf/internal/proxy" "sneak.berlin/go/smallwebwaf/internal/servewaf"
"sneak.berlin/go/smallwebwaf/internal/smallwebwaf" "sneak.berlin/go/smallwebwaf/internal/smallwebwaf"
) )
@@ -1627,7 +1627,7 @@ func statusFrom(t *testing.T, url, from string) int {
} }
req.Header.Set("X-Forwarded-For", from) req.Header.Set("X-Forwarded-For", from)
req.Header.Set(proxy.ClientHeader, from) req.Header.Set(servewaf.ClientHeader, from)
req.Header.Set("X-Request-ID", rand.Text()) req.Header.Set("X-Request-ID", rand.Text())
transport := &http.Transport{} transport := &http.Transport{}
-216
View File
@@ -1,216 +0,0 @@
#!/bin/sh
# script/gitea-setup: build the image, bring up deploy/gitea under a project
# name of its own, and check it:
# - that a page load, a push and a clone sent through traefik pass through
# rproxy, and that servewaf checks them;
# - that gitea logs the visitor's address, not rproxy's;
# - that a request from a container on the shared network skips the checks;
# - that with servewaf stopped, gitea still answers through rproxy.
# It publishes no port of traefik's, since this host's ports 80 and 443 may be
# taken. It reaches traefik at its address on the shared network instead, so
# its own requests come from the network's gateway, which servewaf checks like
# any visitor. Two runs at once on one docker host cannot both start, since
# both would use the subnet compose.yaml names. The containers, the network,
# the volumes, the image and the files the script makes are removed however it
# ends; the gitea and traefik images stay, as the base images of a build do.
# It needs network access. script/check does not run this.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
# Named after this run, so that runs in other clones on the same host
# never touch each other's.
NAME="$("$SCRIPT_DIR/projectname")-gitea-setup-$$"
IMAGE="$NAME-image"
NETWORK="${NAME}_shared"
VISITOR="$NAME-visitor"
# The site's host name and gitea's image, as deploy/gitea/compose.yaml names
# them. gitea's image also runs the container on the shared network.
SITE="git.example.org"
# gitea 1.27.3, 2026-08-29
GITEA_IMAGE="gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef"
# gitea's one user, who owns the repository.
PASSWORD="owner-password"
# The files the script makes, gitea's secret key for this run, and traefik's
# and the gateway's addresses on the shared network once the setup runs.
WORK=""
secret_key=""
traefik=""
gateway=""
cleanup() {
# So that a signal cannot stop it partway through.
trap '' HUP INT TERM
docker rm --force "$VISITOR" >/dev/null 2>&1 || true
if [ -n "$WORK" ]; then
compose down --volumes --remove-orphans --timeout 5 >/dev/null 2>&1 || true
rm -rf "$WORK"
fi
docker rmi --force "$IMAGE" >/dev/null 2>&1 || true
}
# compose <argument>...: docker compose on the setup, under this run's project
# name, with the image built here, this run's secret key and no port of
# traefik's published.
compose() {
SMALLWEBWAF_IMAGE="$IMAGE" GITEA_SECRET_KEY="$secret_key" \
docker compose --project-name "$NAME" \
--file "$ROOT/deploy/gitea/compose.yaml" --file "$WORK/no-ports.yaml" "$@"
}
fail() {
echo "gitea-setup: $*; the last lines of each container's output:" >&2
compose logs --no-color --tail 30 >&2 || true
exit 1
}
# wait_for <what fails> <command>...: run the command every second until
# it succeeds, for at most a minute.
wait_for() {
failure="$1"
shift
tries=0
until "$@"; do
tries=$((tries + 1))
[ "$tries" -lt 60 ] || fail "$failure"
sleep 1
done
}
# logged <service> <text>...: a line of the service's output holds every text,
# in any order.
logged() {
lines="$(compose logs --no-color --no-log-prefix "$1" 2>&1)"
shift
for text in "$@"; do
lines="$(printf '%s\n' "$lines" | grep -F -e "$text")" || return 1
done
}
# send <path> <curl option>...: send a request for path to the site, at
# traefik's address, and print the status of the answer, 000 for none. traefik
# answers with a certificate it made itself, since the setup names no
# certificate resolver.
send() {
path="$1"
shift
curl --silent --show-error --insecure --max-time 30 --output /dev/null \
--write-out '%{http_code}' --resolve "$SITE:443:$traefik" "$@" \
"https://$SITE$path" || true
}
# routed: traefik passes a request for the site's front page on to gitea.
routed() {
[ "$(send / 2>/dev/null)" = 200 ]
}
# site_git <git argument>...: run git as the owner, without the host's own git
# settings, reaching the site at traefik's address. git gives up on a transfer
# slower than 1000 bytes a second for 30 seconds.
site_git() {
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 git \
-c user.name=owner -c user.email=owner@example.invalid \
-c http.sslVerify=false -c "http.curloptResolve=$SITE:443:$traefik" \
-c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30 "$@"
}
# address_of <service>: print the service's address on the shared network.
address_of() {
docker inspect --format "{{(index .NetworkSettings.Networks \"$NETWORK\").IPAddress}}" \
"$(compose ps --quiet "$1")"
}
# passed_through <method> <path>: rproxy passed a request from the gateway for
# path to gitea on servewaf's allow, and servewaf checked it: its report line
# gives the client's counts and the Core Rule Set's score.
passed_through() {
wait_for "rproxy logged no $1 $2 that servewaf allowed" logged rproxy \
"\"method\":\"$1\"" "\"path\":\"$2\"" "\"client_ip\":\"$gateway\"" \
'"disposition":"allow"'
wait_for "servewaf logged no check of $1 $2" logged servewaf \
'"servewaf_line":"report"' "\"method\":\"$1\"" "\"path\":\"$2\"" \
"\"client_ip\":\"$gateway\"" '"action":"forward"' '"counts":' '"waf_score":'
}
main() {
cd "$ROOT"
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
WORK="$(mktemp -d)"
printf 'services:\n traefik:\n ports: !reset []\n' >"$WORK/no-ports.yaml"
docker build --no-cache -t "$IMAGE" .
secret_key="$(docker run --rm "$GITEA_IMAGE" gitea generate secret SECRET_KEY)"
# rproxy is healthy once gitea takes connections.
compose up --detach --quiet-pull --wait --wait-timeout 120 ||
fail "the setup did not start"
traefik="$(address_of traefik)"
gateway="$(docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' "$NETWORK")"
wait_for "traefik did not route the site to rproxy" routed
compose exec --user git gitea gitea admin user create --admin \
--username owner --password "$PASSWORD" --email owner@example.invalid \
--must-change-password=false >/dev/null || fail "creating gitea's user failed"
status="$(send /api/v1/user/repos --user "owner:$PASSWORD" \
--header "Content-Type: application/json" --data '{"name":"repo"}')"
[ "$status" = 201 ] || fail "creating the repository was answered $status"
status="$(send /owner/repo)"
[ "$status" = 200 ] || fail "the repository's page was answered $status"
passed_through GET /owner/repo
echo "gitea-setup: a page load passes through rproxy, and servewaf checks it"
wait_for "gitea did not log the visitor's address" logged gitea \
"GET /owner/repo for $gateway:"
echo "gitea-setup: gitea logs the visitor's address, not rproxy's"
site_git init --quiet --initial-branch=main "$WORK/repo"
echo "hello" >"$WORK/repo/README.md"
site_git -C "$WORK/repo" add README.md
site_git -C "$WORK/repo" commit --quiet --message "Add a README"
site_git -C "$WORK/repo" push --quiet \
"https://owner:$PASSWORD@$SITE/owner/repo.git" main ||
fail "the push failed"
passed_through POST /owner/repo.git/git-receive-pack
echo "gitea-setup: a push passes through rproxy, and servewaf checks it"
site_git clone --quiet "https://owner:$PASSWORD@$SITE/owner/repo.git" \
"$WORK/clone" || fail "the clone failed"
[ "$(cat "$WORK/clone/README.md")" = hello ] || fail "the clone lacks the README"
passed_through POST /owner/repo.git/git-upload-pack
echo "gitea-setup: a clone passes through rproxy, and servewaf checks it"
# A container on the shared network, as sandcastle is, asks for the site
# by its name, and so reaches traefik from its own address.
status="$(docker run --rm --name "$VISITOR" --network "$NETWORK" \
--entrypoint curl "$GITEA_IMAGE" --silent --insecure --max-time 30 \
--output /dev/null --write-out '%{http_code}' "https://$SITE/api/v1/version" ||
true)"
[ "$status" = 200 ] || fail "the request from the shared network was answered $status"
wait_for "servewaf logged no request from the shared network" logged servewaf \
'"servewaf_line":"report"' '"path":"/api/v1/version"' '"action":"forward"'
line="$(compose logs --no-color --no-log-prefix servewaf |
grep -F '"servewaf_line":"report"' | grep -F '"path":"/api/v1/version"')"
case "$line" in
*"\"client_ip\":\"$gateway\""*)
fail "the request from the shared network came from the gateway" ;;
*'"counts":'* | *'"waf_score":'*)
fail "servewaf checked the request from the shared network" ;;
esac
echo "gitea-setup: a request from the shared network skips the checks"
compose stop servewaf >/dev/null 2>&1 || fail "servewaf did not stop"
status="$(send /owner/repo)"
[ "$status" = 200 ] ||
fail "with servewaf stopped, the repository's page was answered $status"
wait_for "rproxy logged no request SWRP_DEFAULT_DISPOSITION decided" \
logged rproxy '"path":"/owner/repo"' '"default_disposition":true'
echo "gitea-setup: with servewaf stopped, gitea answers through rproxy"
}
main "$@"