1 Commits
Author SHA1 Message Date
sneak c8c7b98645 Core Rule Set: Forgejo's ordinary requests pass by default (closes #342)
check / check (push) Waiting to run
Forgejo 17.0.1's recorded requests are in
testdata/forgejo-requests.txt, checked at the defaults.

What Forgejo sends and gitea does not:
- its flash cookie is named flash, which joins gitea_flash among the
  cookies left out;
- its wiki search, whose q skips five rules in forgejo.conf, the
  fifteenth change.

Gaps in gitea's fixes the recording showed: q on the name searches
skips 932250 (ls -la), and the API's package search is one of them.
User and organisation pages, any one-segment path, keep 932250.

Model: opus-5-5
2026-11-11 08:38:08 +00:00
42 changed files with 1401 additions and 2042 deletions

No files matched your search

+18 -31
View File
@@ -293,8 +293,7 @@ Every setting the code reads, with its default:
| `SWW_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. | | `SWW_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. |
| `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. | | `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
| `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. | | `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
| `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state and cache file is written. | | `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state file is written. |
| `SWW_CACHE_DIR` | `/var/cache/sww` | The directory of the cache files, an absolute path. |
| `SWW_LOG_LEVEL` | `info` | The least severe of `waf`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. | | `SWW_LOG_LEVEL` | `info` | The least severe of `waf`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
| `SWW_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. | | `SWW_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
| `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `waf` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. | | `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `waf` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
@@ -581,7 +580,7 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
front of a code forge, bodies carry what people write and publish, which the front of a code forge, bodies carry what people write and publish, which the
rules take for attacks. rules take for attacks.
- `smallwebwaf` changes the Core Rule Set in fourteen ways so that ordinary - `smallwebwaf` changes the Core Rule Set in fifteen ways so that ordinary
requests get through to the apps listed below. No setting undoes the changes; requests get through to the apps listed below. No setting undoes the changes;
"Attack detection" in [`SPEC.md`](SPEC.md) lists them. "Attack detection" in [`SPEC.md`](SPEC.md) lists them.
- These apps work out of the box at the default settings, and their recorded - These apps work out of the box at the default settings, and their recorded
@@ -589,6 +588,9 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
- gitea: git over HTTP and sign-in through Git Credential Manager get - gitea: git over HTTP and sign-in through Git Credential Manager get
through. On gitea's own paths, its searches, file names and branch names through. On gitea's own paths, its searches, file names and branch names
do too. do too.
- Forgejo: git over HTTP and its container and package registries get
through. On Forgejo's own paths, its searches, file names and branch names
do too.
- WordPress: saving a post from the block editor gets through. On - WordPress: saving a post from the block editor gets through. On
WordPress's own paths, its searches, names and file names do too. WordPress's own paths, its searches, names and file names do too.
- Drupal: on Drupal's own paths, the page to return to, its searches and its - Drupal: on Drupal's own paths, the page to return to, its searches and its
@@ -615,29 +617,15 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
while `waf` runs. The image ships one file of probes no real visitor sends while `waf` runs. The image ships one file of probes no real visitor sends
(see [`docs/rules.md`](docs/rules.md)). (see [`docs/rules.md`](docs/rules.md)).
### State and cache files ### State files
`waf` keeps everything in memory, and JSON files hold a copy: `waf` keeps everything in memory, and the five JSON files in `SWW_STATE_DIR`
hold a copy: `bans.json`, `clients.json`, `lookups.json`, `reputation.json` and
- The state files, in `SWW_STATE_DIR`, hold what cannot be fetched again: `alerts.json`. Each is written whole, to a temporary file renamed over the real
- `bans.json`; one, so a crash leaves a complete file. `waf` notices a file edited on disk and
- `clients.json`; takes the edit in; an edit that does not parse is set aside as `<name>.bad`, and
- `abuseipdb-checks-spent.json`; never stops the running process (see
- `alerts.json`. [`docs/state-files.md`](docs/state-files.md)).
- The cache files, in `SWW_CACHE_DIR`, hold answers from outside services:
- `lookups.json`, from GeoJS;
- `lists.json`, the lists fetched by URL;
- `dnsbl.json`, from the DNS blocklists;
- `abuseipdb-scores.json`, from AbuseIPDB;
- `routewatch.json`, from routewatch.
Back up the state directory. The cache directory can be left out of backups: a
lost cache file starts empty and fills again.
Each file is written whole, to a temporary file renamed over the real one, so a
crash leaves a complete file. `waf` notices a file edited on disk and takes the
edit in. An edit that does not parse is set aside as `<name>.bad`, and never
stops the running process (see [`docs/state-files.md`](docs/state-files.md)).
### The image and its processes ### The image and its processes
@@ -648,7 +636,7 @@ stops the running process (see [`docs/state-files.md`](docs/state-files.md)).
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532) - runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
and starts it again when it exits. The `run` script of `waf` gives the state and starts it again when it exits. The `run` script of `waf` gives the state
directory to the `sww` user at each start, so a volume needs no change of directory to the `sww` user at each start, so a volume needs no change of
owner. It makes the cache directory for that user too. owner.
- `rproxy` and `waf` use no token between them. `waf`'s own endpoints, under - `rproxy` and `waf` use no token between them. `waf`'s own endpoints, under
`/_sww/`, are for your admin tools and your metrics collector, at its own `/_sww/`, are for your admin tools and your metrics collector, at its own
address on the private network; nothing routes the internet there. address on the private network; nothing routes the internet there.
@@ -673,8 +661,8 @@ stops the running process (see [`docs/state-files.md`](docs/state-files.md)).
`internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or `internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or
source each. source each.
- `internal/state`, `internal/requestlog`, `internal/remotelog` and - `internal/state`, `internal/requestlog`, `internal/remotelog` and
`internal/metrics`: the state and cache files, the log lines, the syslog `internal/metrics`: the state files, the log lines, the syslog sender and the
sender and the metrics. metrics.
## Rationale ## Rationale
@@ -754,7 +742,6 @@ on the host.
`http://127.0.0.1:8081`; `http://127.0.0.1:8081`;
- `waf` listens on port 8090; - `waf` listens on port 8090;
- its state files go in `bin/state`; - its state files go in `bin/state`;
- its cache files go in `bin/cache`;
- its rule files are those of `share/rules.d`. - its rule files are those of `share/rules.d`.
- `script/example-app`: builds the image and the example app - `script/example-app`: builds the image and the example app
(`deploy/example-app`) on it, runs the app with `rproxy` in its container and (`deploy/example-app`) on it, runs the app with `rproxy` in its container and
@@ -791,8 +778,8 @@ on the host.
- [`docs/alerts.md`](docs/alerts.md): the alert events and what each destination - [`docs/alerts.md`](docs/alerts.md): the alert events and what each destination
gets. gets.
- [`docs/metrics.md`](docs/metrics.md): the metrics. - [`docs/metrics.md`](docs/metrics.md): the metrics.
- [`docs/state-files.md`](docs/state-files.md): the state and cache files and - [`docs/state-files.md`](docs/state-files.md): the state files and what each
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.
## License ## License
+89 -102
View File
@@ -37,7 +37,7 @@ never as a time.
`SWW_`, and those of `rproxy` with `SWRP_`. The only other inputs are `waf`'s `SWW_`, and those of `rproxy` with `SWRP_`. The only other inputs are `waf`'s
files: files:
- the rule files; - the rule files;
- the state files and the cache files; - the state files;
- a lookup database; - a lookup database;
- a `SWW_` setting given as a file; - a `SWW_` setting given as a file;
- the file `SWW_LOG_REMOTE_TLS_CA_FILE` names; - the file `SWW_LOG_REMOTE_TLS_CA_FILE` names;
@@ -56,19 +56,19 @@ never as a time.
## Architecture ## Architecture
- One statically linked Go binary. Only `waf` writes to disk, and only to its - One statically linked Go binary. Only `waf` writes to disk, and only to its
state directory and its cache directory. It ships in one image, which runs state directory. It ships in one image, which runs either command (see
either command (see "Deployment"). "Deployment").
- No database of any kind. Nothing is read from disk while serving a request. - No database of any kind. Nothing is read from disk while serving a request.
- Packages: - Packages:
- `cmd/sww`: the binary, which only calls `internal/sww`. - `cmd/sww`: the binary, which only calls `internal/sww`.
- `internal/sww`: the process, as the command it is given. `sww waf` reads - `internal/sww`: the process, as the command it is given. `sww waf` reads
the settings, the rule files, the lookup database and the state and cache the settings, the rule files, the lookup database and the state files,
files, listens, and serves until `SIGTERM` or `SIGINT`. Then it stops, listens, and serves until `SIGTERM` or `SIGINT`. Then it stops, writing
writing the state and cache files (see "`waf`"). `sww rproxy` reads only the state files (see "`waf`"). `sww rproxy` reads only that command's
that command's settings, and runs `internal/rproxy` until `SIGTERM` or settings, and runs `internal/rproxy` until `SIGTERM` or `SIGINT` (see
`SIGINT` (see "`rproxy`"). `sww healthcheck` is the image's health check. "`rproxy`"). `sww healthcheck` is the image's health check. With no
With no command, or one it does not know, the binary prints the commands command, or one it does not know, the binary prints the commands and exits
and exits with status 2. with 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/waf`: `waf`'s server. It runs the checks in `check`, and holds - `internal/waf`: `waf`'s server. It runs the checks in `check`, and holds
`waf`'s question, answer and report. It answers `waf`'s own endpoints in `waf`'s question, answer and report. It answers `waf`'s own endpoints in
@@ -84,8 +84,8 @@ never as a time.
of bans is full. of bans is full.
- `internal/rules`: reads the rule files at start and again when they - `internal/rules`: reads the rule files at start and again when they
change, and tells which rules a request matches. change, and tells which rules a request matches.
- `internal/coreruleset`: the Core Rule Set, with the fourteen changes - `internal/coreruleset`: the Core Rule Set, with the fifteen changes listed
listed under "Attack detection", as Coraza directives. under "Attack detection", as Coraza directives.
- `internal/lookup`: looks clients up through GeoJS or in the lookup - `internal/lookup`: looks clients up through GeoJS or in the lookup
database. database.
- `internal/reputation`: fetches the lists, asks the DNS blocklists and - `internal/reputation`: fetches the lists, asks the DNS blocklists and
@@ -96,8 +96,8 @@ never as a time.
- `internal/alerts`: holds back repeats and alerts past the hourly limit, - `internal/alerts`: holds back repeats and alerts past the hourly limit,
and sends the rest to the webhook, Slack and ntfy, each from a queue of and sends the rest to the webhook, Slack and ntfy, each from a queue of
its own. its own.
- `internal/state`: reads the state and cache files at start, takes in edits - `internal/state`: reads the state files at start, takes in edits while
while running, and writes the files when due and at the stop. running, and writes the files when due and at the stop.
- `internal/requestlog` and `internal/remotelog`: the lines on stdout, and - `internal/requestlog` and `internal/remotelog`: the lines on stdout, and
sending them to the syslog server. sending them to the syslog server.
- `internal/metrics`: the metrics, in the Prometheus text format. - `internal/metrics`: the metrics, in the Prometheus text format.
@@ -106,7 +106,7 @@ never as a time.
container, which `script/example-app` checks with `waf` in another. container, which `script/example-app` checks with `waf` in another.
- Libraries, checked against the owner's Go dependency defaults: - Libraries, checked against the owner's Go dependency defaults:
- the standard library for the reverse proxy, HTTP clients, DNS, logging - the standard library for the reverse proxy, HTTP clients, DNS, logging
(`log/slog`) and the state and cache files (`encoding/json`, `os.Rename`); (`log/slog`) and the state files (`encoding/json`, `os.Rename`);
- `github.com/corazawaf/coraza/v3` and - `github.com/corazawaf/coraza/v3` and
`github.com/corazawaf/coraza-coreruleset/v4` at `v4.25.0`, which carries `github.com/corazawaf/coraza-coreruleset/v4` at `v4.25.0`, which carries
the Core Rule Set 4.25.0. The changes `smallwebwaf` makes to it, and the the Core Rule Set 4.25.0. The changes `smallwebwaf` makes to it, and the
@@ -367,7 +367,7 @@ below are `waf`'s settings; "`rproxy`" gives the use of `rproxy`'s. Conventions:
one, since `rproxy` never touches disk. one, since `rproxy` never touches disk.
- Settings, files included, are read once at start. Changing one means - Settings, files included, are read once at start. Changing one means
restarting the container. The files `waf` watches while it runs are its state restarting the container. The files `waf` watches while it runs are its state
and cache files, its rule files and the lookup database. files, its rule files and the lookup database.
- An invalid setting stops the process at start with one log line, marked - An invalid setting stops the process at start with one log line, marked
`"type":"process"` with `"msg":"invalid setting"`. The line names the variable `"type":"process"` with `"msg":"invalid setting"`. The line names the variable
and what is wrong with the value; "Each container's processes" says what and what is wrong with the value; "Each container's processes" says what
@@ -397,10 +397,10 @@ below are `waf`'s settings; "`rproxy`" gives the use of `rproxy`'s. Conventions:
start. The file form keeps a token out of the container's environment (see start. The file form keeps a token out of the container's environment (see
"Tokens" under "Deployment"). "Tokens" under "Deployment").
### State and cache files ### State files
- `SWW_STATE_DIR` names the directory of the state files, and `SWW_CACHE_DIR` - `SWW_STATE_DIR` names the directory of the state files (see "State" under
that of the cache files (see "State" under "Deployment"). "Deployment").
- `bans.json` is written `SWW_STATE_WRITE_DELAY` after a ban is made, lifted or - `bans.json` is written `SWW_STATE_WRITE_DELAY` after a ban is made, lifted or
made permanent, so a burst of changes becomes one write. Every file is written made permanent, so a burst of changes becomes one write. Every file is written
every `SWW_STATE_COUNTER_INTERVAL`, and at an orderly stop. every `SWW_STATE_COUNTER_INTERVAL`, and at an orderly stop.
@@ -583,7 +583,7 @@ Each tag and where it comes from:
- They are added before `deny_nets`, so the log line of a request it refused - They are added before `deny_nets`, so the log line of a request it refused
has them. has them.
- `waf` asks routewatch in the background and never waits for it. An answer - `waf` asks routewatch in the background and never waits for it. An answer
is kept for 1 hour, in memory and in `routewatch.json`. is kept for 1 hour, in memory only.
- After a query fails, routewatch is not asked for 1 minute. - After a query fails, routewatch is not asked for 1 minute.
- A request without these tags gives the reason in the log field - A request without these tags gives the reason in the log field
`routewatch_tags_missing` (see `routewatch_tags_missing` (see
@@ -681,7 +681,7 @@ files, the limits and the bans still apply to the client that sends it.
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` names a file of exclusions, written as - `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` names a file of exclusions, written as
Coraza directives. An exclusion switches one rule off for some fields or paths Coraza directives. An exclusion switches one rule off for some fields or paths
only, where `SWW_WAF_DISABLED_RULES` switches it off everywhere. The file only, where `SWW_WAF_DISABLED_RULES` switches it off everywhere. The file
holds an app's own exclusions. The eighth to fourteenth changes below hold holds an app's own exclusions. The eighth to fifteenth changes below hold
those of the apps they name. those of the apps they name.
- `waf` loads the file after `smallwebwaf`'s own setup rules and before the - `waf` loads the file after `smallwebwaf`'s own setup rules and before the
Core Rule Set's rules. The Core Rule Set loads its own Core Rule Set's rules. The Core Rule Set loads its own
@@ -728,10 +728,11 @@ files, the limits and the bans still apply to the client that sends it.
scanner from an ordinary client. In front of a code forge, registry and git scanner from an ordinary client. In front of a code forge, registry and git
clients get `404` and `401` by design, hundreds of times a minute. clients get `404` and `401` by design, hundreds of times a minute.
`smallwebwaf` changes the Core Rule Set 4.25.0 in fourteen ways. Without them, `smallwebwaf` changes the Core Rule Set 4.25.0 in fifteen ways. Without them, it
it would refuse ordinary requests to these apps: would refuse ordinary requests to these apps:
- gitea; - gitea;
- Forgejo;
- WordPress; - WordPress;
- Drupal; - Drupal;
- MediaWiki; - MediaWiki;
@@ -773,22 +774,23 @@ The changes hold in front of every app, and no setting undoes them:
catch reaches the app, such as the command `|cat /etc/passwd`. Path catch reaches the app, such as the command `|cat /etc/passwd`. Path
traversal, SQL and script injection, and PHP, Java and Node.js code are traversal, SQL and script injection, and PHP, Java and Node.js code are
still refused there. Other parameters keep all three rules. The eighth to still refused there. Other parameters keep all three rules. The eighth to
fourteenth changes switch some of them off for some fields on the paths of fifteenth changes switch some of them off for some fields on the paths of
the apps they name. An app that uses one of these parameters as a file on the apps they name. An app that uses one of these parameters as a file on
the server, or passes it to a shell, must check the value itself. A rule the server, or passes it to a shell, must check the value itself. A rule
file can refuse it instead. file can refuse it instead.
5. Two cookies are left out, and `Referer` keeps fewer rules: 5. Three cookies are left out, and `Referer` keeps fewer rules:
- The Core Rule Set reads the request without the `gitea_flash` and - The Core Rule Set reads the request without the `gitea_flash`, `flash` and
`redirect_to` cookies. gitea puts a message naming what was just done into `redirect_to` cookies. gitea puts a message naming what was just done into
`gitea_flash`. It puts into `redirect_to` the page to return to after `gitea_flash`, and Forgejo puts it into `flash`. Both put into
signing in. The Core Rule Set takes file names in them, such as `redirect_to` the page to return to after signing in. The Core Rule Set
`package.json`, for attacks. A browser sends such a cookie with every takes file names in them, such as `package.json`, for attacks. A browser
request until gitea replaces it. gitea cannot replace it while those sends such a cookie with every request until the app replaces it. The app
requests are refused, so one refusal would keep the browser out of the cannot replace it while those requests are refused, so one refusal would
whole site. gitea shows the message with any script removed and returns keep the browser out of the whole site. gitea and Forgejo show the message
only to its own pages. Every other cookie is read in full. An app that with any script removed and return only to their own pages. Every other
reads a cookie of either name must check it itself. cookie is read in full. An app that reads a cookie of one of these names
must check it itself.
- `Referer` is not checked for a Unix command given without arguments - `Referer` is not checked for a Unix command given without arguments
(932340), for Java starting a process (944110) or for a Java class name (932340), for Java starting a process (944110) or for a Java class name
(944130). `Referer` names the page a request comes from, on everything (944130). `Referer` names the page a request comes from, on everything
@@ -817,17 +819,19 @@ The changes hold in front of every app, and no setting undoes them:
`SELECT * FROM users WHERE` or `../`. `SELECT * FROM users WHERE` or `../`.
- `Referer` of a page opened from such a search, for script and SQL - `Referer` of a page opened from such a search, for script and SQL
injection and Java code (941110, 941120, 941160, 942100, 944100). injection and Java code (941110, 941120, 941160, 942100, 944100).
- `q` on gitea's searches for a name, for the names of system files and - `q` on gitea's searches for a name, for the lists of system files and
commands (930120, 932120, 932260). Names such as `.profile` and command names (930120, 932120, 932250, 932260). Names such as `.profile`
`python3-app` were refused. gitea searches for the names of: and `python3-app`, and the words `ls -la`, were refused. gitea searches
for the names of:
- repositories; - repositories;
- users and organisations; - users and organisations;
- branches and tags; - branches and tags;
- packages. - packages, on its pages and in its API.
A user's or an organisation's page is any path of one segment, so on A user's or an organisation's page is any path of one segment, so on
every such path, on any app, `q` skips these three rules, for example on every such path, on any app, `q` skips 930120, 932120 and 932260, for
`/search`. example on `/search`. It keeps 932250 there, so `ls -la` is refused on
those pages.
Some of these searches also skip the rule for Java class names (944130), Some of these searches also skip the rule for Java class names (944130),
which refused `org.apache.commons.logging`. which refused `org.apache.commons.logging`.
@@ -1566,6 +1570,22 @@ The changes hold in front of every app, and no setting undoes them:
`/index.php/`. Elsewhere these fields keep every rule. An app other than `/index.php/`. Elsewhere these fields keep every rule. An app other than
Nextcloud that serves one of these paths must check these fields itself. Nextcloud that serves one of these paths must check these fields itself.
15. On Forgejo's paths, `q` on the search of a wiki skips the rules that refused
ordinary Forgejo searches there. Forgejo is a fork of gitea, and the eighth
change applies to its requests too. This change holds only what Forgejo
sends and gitea does not. `internal/coreruleset/forgejo.conf` lists each
rule and the search it refused:
- system files (930120), such as `.htaccess`;
- Windows commands (932120), such as `powershell`;
- shell paths (932160), such as `/bin/sh`;
- Unix commands (932250), such as `ls -la`;
- command names (932260), such as `python3`.
`; wget http://example.com/x.sh`, path traversal, and SQL and script
injection are still refused there. The path is `/<owner>/<repo>/wiki/search`
at the site root. An app other than Forgejo that serves such a path must
check `q` itself.
The parameter and cookie names in these changes are matched without regard to The parameter and cookie names in these changes are matched without regard to
case, as Coraza matches them. case, as Coraza matches them.
@@ -1601,8 +1621,8 @@ together.
is kept whole, comment lines included, on failure and across restarts. is kept whole, comment lines included, on failure and across restarts.
- Each list is fetched again `SWW_BLOCKLIST_REFRESH` after its last try, one - Each list is fetched again `SWW_BLOCKLIST_REFRESH` after its last try, one
list at a time. `lists.json` keeps when each was last tried, so a restart does list at a time. `reputation.json` keeps when each was last tried, so a restart
not fetch a list again before its time. A fetch fails when: does not fetch a list again before its time. A fetch fails when:
- the server answers other than `200`; - the server answers other than `200`;
- the fetch does not finish within a minute; - the fetch does not finish within a minute;
- the list is longer than 16 MiB; - the list is longer than 16 MiB;
@@ -1646,9 +1666,9 @@ together.
DNS blocklists made for mail servers list residential ranges, and would DNS blocklists made for mail servers list residential ranges, and would
block ordinary visitors, so they should not be used. Spamhaus zones need block ordinary visitors, so they should not be used. Spamhaus zones need
their keyed query service, given as a zone name that contains the key. `waf` their keyed query service, given as a zone name that contains the key. `waf`
shows the key as `********` wherever it names the zone. Only `dnsbl.json` shows the key as `********` wherever it names the zone. Only
keeps the key. `SWW_DNSBL_RESOLVER` names a resolver, since public resolvers `reputation.json` keeps the key. `SWW_DNSBL_RESOLVER` names a resolver,
are refused by several list operators. since public resolvers are refused by several list operators.
- AbuseIPDB (`SWW_ABUSEIPDB_KEY`) is asked, at its check endpoint, for the abuse - AbuseIPDB (`SWW_ABUSEIPDB_KEY`) is asked, at its check endpoint, for the abuse
confidence score of a client, from 0 to 100. Only a client that has already confidence score of a client, from 0 to 100. Only a client that has already
@@ -1659,11 +1679,11 @@ together.
client's later requests. For an IPv6 client, AbuseIPDB is asked about the client's later requests. For an IPv6 client, AbuseIPDB is asked about the
address of the request that set off the check, and the score is used for the address of the request that set off the check, and the score is used for the
whole group. A score at or over `SWW_ABUSEIPDB_MIN_SCORE` is a hit. The budget whole group. A score at or over `SWW_ABUSEIPDB_MIN_SCORE` is a hit. The budget
is counted in UTC days and kept in `abuseipdb-checks-spent.json`, so a restart is counted in UTC days and kept in `reputation.json`, so a restart does not
does not make it whole again. The check that spends the last of it is logged make it whole again. The check that spends the last of it is logged and
and alerted, and no client is checked until the day ends. The key is sent in alerted, and no client is checked until the day ends. The key is sent in the
the `Key` header and nowhere else, and is never logged. A check fails, after `Key` header and nowhere else, and is never logged. A check fails, after which
which no client is checked for a minute, when: no client is checked for a minute, when:
- AbuseIPDB answers other than `200`; - AbuseIPDB answers other than `200`;
- the answer gives no score; - the answer gives no score;
- no answer comes within `SWW_REPUTATION_TIMEOUT`. - no answer comes within `SWW_REPUTATION_TIMEOUT`.
@@ -1671,9 +1691,9 @@ together.
by default. Such verdicts are less certain than a blocklist, so a listed by default. Such verdicts are less certain than a blocklist, so a listed
client gets a quarter of every request and byte limit rather than a refusal. client gets a quarter of every request and byte limit rather than a refusal.
- Verdicts and scores are kept for `SWW_REPUTATION_CACHE_TTL`, in memory and in - Verdicts and scores are kept for `SWW_REPUTATION_CACHE_TTL`, in memory and in
`dnsbl.json` and `abuseipdb-scores.json`. A source that is down or over quota `reputation.json`. A source that is down or over quota gives no verdict. The
gives no verdict. The service continues. Each failure raises a service continues. Each failure raises a `source_failure` alert (see
`source_failure` alert (see "Alerting"). "Alerting").
### Alerting ### Alerting
@@ -1776,43 +1796,21 @@ default file.
## Persistent state ## Persistent state
All state lives in memory, and JSON files hold a copy of it, so an orderly stop All state lives in memory, and the files in `SWW_STATE_DIR` hold a copy of it,
loses nothing. The files are meant for people as well: an admin can read or edit so an orderly stop loses nothing. The files are meant for people as well: an
them at any time, and the running `waf` takes the edit in. `docs/state-files.md` admin can read or edit them at any time, and the running `waf` takes the edit
lists the files, what each holds, and their sizes. in. `docs/state-files.md` lists the files, what each holds, and their sizes.
- The state files, in `SWW_STATE_DIR`, hold what cannot be fetched again:
- `bans.json`;
- `clients.json`;
- `abuseipdb-checks-spent.json`;
- `alerts.json`.
- The cache files, in `SWW_CACHE_DIR`, hold the answers of outside services:
- `lookups.json`;
- `lists.json`;
- `dnsbl.json`;
- `abuseipdb-scores.json`;
- `routewatch.json`.
- A lost cache file costs only questions asked again, so the cache directory can
be left out of backups.
- Each file holds one kind of state, as JSON with: - Each file holds one kind of state, as JSON with:
- a top-level `version` number; - a top-level `version` number;
- entries sorted by client address; - entries sorted by client address;
- times in RFC 3339 UTC; - times in RFC 3339 UTC;
- days as `YYYY-MM-DD`; - days as `YYYY-MM-DD`;
- durations and sizes as plain numbers, with the unit in the field name. - durations and sizes as plain numbers, with the unit in the field name.
- These files are indented, so that a person can open `bans.json` in an editor, - `bans.json`, `reputation.json` and `alerts.json` are indented, so that a
find an address, and remove or add an entry: person can open `bans.json` in an editor, find an address, and remove or add
- `bans.json`; an entry. `clients.json` and `lookups.json` put each entry on one line, which
- `abuseipdb-checks-spent.json`; halves their size and lets `grep` show everything about one client.
- `alerts.json`;
- `lists.json`.
- The others put each entry on one line, which halves their size and lets `grep`
show everything about one client:
- `clients.json`;
- `lookups.json`;
- `dnsbl.json`;
- `abuseipdb-scores.json`;
- `routewatch.json`.
- Clients that were never banned are kept too, so every client's history - Clients that were never banned are kept too, so every client's history
survives a restart. survives a restart.
- Each file has a bound, so it can be rewritten whole. Writing one takes well - Each file has a bound, so it can be rewritten whole. Writing one takes well
@@ -1826,8 +1824,8 @@ lists the files, what each holds, and their sizes.
disk, so an admin's edit is never overwritten. disk, so an admin's edit is never overwritten.
- At start, the files are read back. Active bans go into an in-memory lookup by - At start, the files are read back. Active bans go into an in-memory lookup by
netblock, everything else into maps. Expired cache entries are discarded. A netblock, everything else into maps. Expired cache entries are discarded. A
missing file means empty state, and is normal on first run. In a state file, missing file means empty state, and is normal on first run. These stop the
these stop the process with a message naming the file and position: process with a message naming the file and position:
- a file that does not parse, or has an unknown `version`; - a file that does not parse, or has an unknown `version`;
- an entry without a field it needs; - an entry without a field it needs;
- a ban whose cause is not one of the four; - a ban whose cause is not one of the four;
@@ -1837,11 +1835,6 @@ lists the files, what each holds, and their sizes.
Since writes are atomic, a broken file can only come from a hand edit, which Since writes are atomic, a broken file can only come from a hand edit, which
the editor should hear about. the editor should hear about.
- A cache file never stops the start. One that does not parse is set aside as
`<name>.bad`, as an edit is below. One that cannot be read is logged and
written over at its next write. Either way its cache starts empty and fills
again.
- While running, `waf` notices a file that is edited, replaced or added. It - While running, `waf` notices a file that is edited, replaced or added. It
tells its own writes from an admin's by comparing the file with what it last tells its own writes from an admin's by comparing the file with what it last
read or wrote. An edit that parses is taken in at once: what the file says read or wrote. An edit that parses is taken in at once: what the file says
@@ -1932,10 +1925,10 @@ each app. `rproxy` asks `waf` about each visitor's request. `waf` answers
This section is the agreement between the two. The header names, the fields and This section is the agreement between the two. The header names, the fields and
the statuses below are exact. the statuses below are exact.
`waf` reads the settings, the rule files, the lookup database and the state and `waf` reads the settings, the rule files, the lookup database and the state
cache files, and listens at `SWW_LISTEN_ADDR`. On `SIGTERM` or `SIGINT` it stops files, and listens at `SWW_LISTEN_ADDR`. On `SIGTERM` or `SIGINT` it stops
taking requests and gives those in progress five seconds to finish. It then taking requests and gives those in progress five seconds to finish. It then
writes the state and cache files. writes the state files.
### The question ### The question
@@ -2508,10 +2501,8 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
`SWW_COMMAND` is `waf`, and `rproxy` when it is `rproxy`, unset or empty. It `SWW_COMMAND` is `waf`, and `rproxy` when it is `rproxy`, unset or empty. It
runs either as the `sww` user (uid and gid 65532), with runit's `chpst`. For runs either as the `sww` user (uid and gid 65532), with runit's `chpst`. For
`waf`, it first makes `SWW_STATE_DIR` and every file in it belong to that `waf`, it first makes `SWW_STATE_DIR` and every file in it belong to that
user. It also makes `SWW_CACHE_DIR` if it is missing, and gives it and every user. For `rproxy` it changes no owner. With any other value, it starts
file in it to that user. For `rproxy` it changes no owner. With any other nothing, and stops with a message naming `SWW_COMMAND` and its value.
value, it starts nothing, and stops with a message naming `SWW_COMMAND` and
its value.
Every `run` script, the app's included, is a bash script. It starts with Every `run` script, the app's included, is a bash script. It starts with
`#!/usr/bin/env bash` and `set -euo pipefail`, and puts its code in a `main` `#!/usr/bin/env bash` and `set -euo pipefail`, and puts its code in a `main`
@@ -2534,7 +2525,7 @@ process that stops at start is started again every second, not in a tight loop.
- invalid configuration; - invalid configuration;
- a lookup database that is missing or unreadable; - a lookup database that is missing or unreadable;
- a rule file or state file that does not parse; - a rule file or state file that does not parse;
- an unwritable `SWW_STATE_DIR` or `SWW_CACHE_DIR`; - an unwritable `SWW_STATE_DIR`;
- a `SWW_RULES_DIR` that does not exist or cannot be read; - a `SWW_RULES_DIR` that does not exist or cannot be read;
- a `SWW_LISTEN_ADDR` it cannot listen on. - a `SWW_LISTEN_ADDR` it cannot listen on.
- Only an invalid setting, or a `SWRP_LISTEN_ADDR` it cannot listen on, stops - Only an invalid setting, or a `SWRP_LISTEN_ADDR` it cannot listen on, stops
@@ -2603,10 +2594,6 @@ needs no backup beyond the host's own. The image declares no volume, since every
app image built on it would inherit it. `rproxy` keeps nothing, and needs no app image built on it would inherit it. `rproxy` keeps nothing, and needs no
volume. volume.
`waf` keeps its cache files in `SWW_CACHE_DIR`, `/var/cache/sww` by default,
which the `run` script makes. The cache directory needs no volume and can be
left out of backups: a new container starts with empty caches, which fill again.
### Tokens ### Tokens
A token given as a file (`SWW_ADMIN_TOKEN_FILE`, `SWW_METRICS_TOKEN_FILE`) stays A token given as a file (`SWW_ADMIN_TOKEN_FILE`, `SWW_METRICS_TOKEN_FILE`) stays
+1 -2
View File
@@ -23,8 +23,7 @@ back or dropped.
- the day's AbuseIPDB checks used up. - the day's AbuseIPDB checks used up.
- `file_error`: one of: - `file_error`: one of:
- a rule file or state file edit with an error; - a rule file or state file edit with an error;
- a cache file that does not parse, at start or after an edit; - a state file that cannot be written;
- a state file or cache file that cannot be written;
- a replacement lookup database that cannot be read. - a replacement lookup database that cannot be read.
- `summary`: the hourly summary of alerts held back. - `summary`: the hourly summary of alerts held back.
+2 -2
View File
@@ -58,9 +58,9 @@ By `source`: each list's URL, each zone, and `abuseipdb`:
- `sww_reputation_last_fetch_timestamp_seconds`, for a list; - `sww_reputation_last_fetch_timestamp_seconds`, for a list;
- `sww_reputation_daily_budget_remaining`, for AbuseIPDB. - `sww_reputation_daily_budget_remaining`, for AbuseIPDB.
## State and cache files ## State files
By `file`, the name of a state or cache file: By `file`:
- `sww_state_file_writes_total`, `sww_state_file_write_failures_total`, - `sww_state_file_writes_total`, `sww_state_file_write_failures_total`,
`sww_state_file_last_write_timestamp_seconds` and `sww_state_file_size_bytes`; `sww_state_file_last_write_timestamp_seconds` and `sww_state_file_size_bytes`;
+3 -5
View File
@@ -50,16 +50,14 @@ the two (see "The image's command" below).
- `SWW_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least - `SWW_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least
32 characters. 32 characters.
## State and cache files ## State files
- `SWW_STATE_DIR` (default `/var/lib/sww`): the directory of the state files, an - `SWW_STATE_DIR` (default `/var/lib/sww`): the directory of the state files, an
absolute path that exists and can be written. absolute path that exists and can be written.
- `SWW_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be - `SWW_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
written after a ban is made, lifted or made permanent. A duration. written after a ban is made, lifted or made permanent. A duration.
- `SWW_STATE_COUNTER_INTERVAL` (default `15m`): how often every state and cache - `SWW_STATE_COUNTER_INTERVAL` (default `15m`): how often every state file is
file is written. A duration. written. A duration.
- `SWW_CACHE_DIR` (default `/var/cache/sww`): the directory of the cache files,
an absolute path that can be written. The image's `run` script makes it.
## Logging ## Logging
+18 -73
View File
@@ -1,23 +1,8 @@
# State and cache files # State files
The JSON files `waf` keeps, and what each holds. "Persistent state" in The five JSON files in `SWW_STATE_DIR`, and what each holds. "Persistent state"
[`SPEC.md`](../SPEC.md) says how they are written, read and edited. Each has a in [`SPEC.md`](../SPEC.md) says how they are written, read and edited. Each has
top-level `version` of 1. a top-level `version` of 1.
- The state files are in `SWW_STATE_DIR`, `/var/lib/sww` by default. They hold
what cannot be fetched again, so back them up:
- `bans.json`;
- `clients.json`;
- `abuseipdb-checks-spent.json`;
- `alerts.json`.
- The cache files are in `SWW_CACHE_DIR`, `/var/cache/sww` by default. They hold
answers from outside services, which are asked again when a file is lost, so
the directory can be left out of backups:
- `lookups.json`;
- `lists.json`;
- `dnsbl.json`;
- `abuseipdb-scores.json`;
- `routewatch.json`.
## `bans.json` ## `bans.json`
@@ -82,13 +67,21 @@ since it was first seen:
`blocked_by_core_rule_set`. A file may also hold `token_refused`, which is `blocked_by_core_rule_set`. A file may also hold `token_refused`, which is
kept, and which nothing adds to. kept, and which nothing adds to.
## `abuseipdb-checks-spent.json` ## `lookups.json`
The AbuseIPDB checks spent today, so that a restart does not spend the daily GeoJS answers, one per line:
budget again:
- `day`: the day, in UTC; - `client`;
- `spent`: the checks spent on it. - `asn`, `as_name` and `country`;
- `answered`: when GeoJS answered;
- `used`: when the answer was last used.
## `reputation.json`
- the last good copy of each list fetched from a URL, with when it was last
tried;
- each DNS blocklist verdict still in use;
- AbuseIPDB's scores, and the checks spent today.
## `alerts.json` ## `alerts.json`
@@ -98,51 +91,6 @@ budget again:
- the alerts sent and held back in the current hour; - the alerts sent and held back in the current hour;
- the alerts waiting for each destination. - the alerts waiting for each destination.
## `lookups.json`
A cache file. GeoJS answers, one per line:
- `client`;
- `asn`, `as_name` and `country`;
- `answered`: when GeoJS answered;
- `used`: when the answer was last used.
## `lists.json`
A cache file. The last good copy of each list fetched from a URL, with when it
was last tried:
- `url`;
- `tried`: when it was last fetched or tried;
- `fetched` and `lines`: when the last good copy was fetched, and its lines,
comment lines included.
## `dnsbl.json`
A cache file. Each DNS blocklist verdict still in use, one per line:
- `zone` and `client`;
- `listed`: whether the zone lists the client;
- `fetched`: when the zone answered.
## `abuseipdb-scores.json`
A cache file. AbuseIPDB's scores still in use, one per line:
- `client`;
- `score`: from 0 to 100;
- `fetched`: when AbuseIPDB answered.
## `routewatch.json`
A cache file. routewatch's answers from the last hour, one per line:
- `client`;
- `netblock`: the netblock the client's address is announced in, empty for an
address without a live route;
- `asn`: the number of the AS that originates the netblock, 0 for none;
- `answered`: when routewatch answered.
## Sizes ## Sizes
- `clients.json`: about 1 KiB per client, about 20 MiB at the default - `clients.json`: about 1 KiB per client, about 20 MiB at the default
@@ -154,7 +102,4 @@ A cache file. routewatch's answers from the last hour, one per line:
more. more.
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when - `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
full. full.
- `routewatch.json`: about 120 bytes per answer, at most about 12 MiB. - `reputation.json` and `alerts.json`: usually a few MiB or less.
- `abuseipdb-checks-spent.json`: a few bytes.
- `lists.json`, `dnsbl.json`, `abuseipdb-scores.json` and `alerts.json`: usually
a few MiB or less.
+3 -5
View File
@@ -56,11 +56,9 @@ const (
// of a list failing, a query to a DNSBL zone or a check with AbuseIPDB // of a list failing, a query to a DNSBL zone or a check with AbuseIPDB
// failing or refused, or the day's AbuseIPDB checks used up. // failing or refused, or the day's AbuseIPDB checks used up.
EventSourceFailure = "source_failure" EventSourceFailure = "source_failure"
// EventFileError is one of: // EventFileError is a rule file or state file edited while sww
// - a rule file or state file edited while sww runs that does not parse; // runs that does not parse, a replacement of the lookup database that
// - a cache file that does not parse, at start or after an edit; // cannot be read, or a state file that cannot be written.
// - a state file or cache file that cannot be written;
// - a replacement of the lookup database that cannot be read.
EventFileError = "file_error" EventFileError = "file_error"
// EventSummary is the summary sent as an hour ends: of the alerts held // EventSummary is the summary sent as an hour ends: of the alerts held
// back in it past SWW_ALERT_MAX_PER_HOUR, and of the repeats held // back in it past SWW_ALERT_MAX_PER_HOUR, and of the repeats held
+3 -7
View File
@@ -191,15 +191,12 @@ type Config struct {
BanScopeV4Prefix int BanScopeV4Prefix int
// StateDir is the directory of the state files, an absolute path // StateDir is the directory of the state files, an absolute path
// (SWW_STATE_DIR). bans.json is written StateWriteDelay after a ban // (SWW_STATE_DIR). bans.json is written StateWriteDelay after a ban
// is made (SWW_STATE_WRITE_DELAY), and every state and cache file // is made (SWW_STATE_WRITE_DELAY), and every state file every
// every StateCounterInterval (SWW_STATE_COUNTER_INTERVAL). Neither // StateCounterInterval (SWW_STATE_COUNTER_INTERVAL). Neither can be
// can be off. // off.
StateDir string StateDir string
StateWriteDelay time.Duration StateWriteDelay time.Duration
StateCounterInterval time.Duration StateCounterInterval time.Duration
// CacheDir is the directory of the cache files, which keep answers
// from outside services, an absolute path (SWW_CACHE_DIR).
CacheDir string
// LogRequestHeaders are the request headers whose values the request // LogRequestHeaders are the request headers whose values the request
// log gives, in lower case (SWW_LOG_REQUEST_HEADERS). // log gives, in lower case (SWW_LOG_REQUEST_HEADERS).
LogRequestHeaders []string LogRequestHeaders []string
@@ -563,7 +560,6 @@ func FromEnvironment(
StateDir: env.absolutePath("SWW_STATE_DIR", "/var/lib/sww"), StateDir: env.absolutePath("SWW_STATE_DIR", "/var/lib/sww"),
StateWriteDelay: env.durationNotOff("SWW_STATE_WRITE_DELAY", "10s"), StateWriteDelay: env.durationNotOff("SWW_STATE_WRITE_DELAY", "10s"),
StateCounterInterval: env.durationNotOff("SWW_STATE_COUNTER_INTERVAL", "15m"), StateCounterInterval: env.durationNotOff("SWW_STATE_COUNTER_INTERVAL", "15m"),
CacheDir: env.absolutePath("SWW_CACHE_DIR", "/var/cache/sww"),
LogRequestHeaders: env.headerNames("SWW_LOG_REQUEST_HEADERS", LogRequestHeaders: env.headerNames("SWW_LOG_REQUEST_HEADERS",
"accept,accept-language,accept-encoding,content-type,origin,range"), "accept,accept-language,accept-encoding,content-type,origin,range"),
LogLevel: env.logLevel("SWW_LOG_LEVEL", "info"), LogLevel: env.logLevel("SWW_LOG_LEVEL", "info"),
+7 -21
View File
@@ -74,7 +74,6 @@ const (
stateDir = "SWW_STATE_DIR" stateDir = "SWW_STATE_DIR"
stateWriteDelay = "SWW_STATE_WRITE_DELAY" stateWriteDelay = "SWW_STATE_WRITE_DELAY"
stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL" stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL"
cacheDir = "SWW_CACHE_DIR"
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
metricsTopN = "SWW_METRICS_TOP_N" metricsTopN = "SWW_METRICS_TOP_N"
@@ -263,7 +262,6 @@ func TestDefaults(t *testing.T) {
StateDir: "/var/lib/sww", StateDir: "/var/lib/sww",
StateWriteDelay: 10 * time.Second, StateWriteDelay: 10 * time.Second,
StateCounterInterval: 15 * time.Minute, StateCounterInterval: 15 * time.Minute,
CacheDir: "/var/cache/sww",
MetricsToken: "", MetricsToken: "",
MetricsTopN: 50, MetricsTopN: 50,
RulesDir: "/etc/sww/rules.d", RulesDir: "/etc/sww/rules.d",
@@ -348,7 +346,6 @@ func TestValuesAsSet(t *testing.T) {
stateDir: "/srv/waf-state", stateDir: "/srv/waf-state",
stateWriteDelay: "500ms", stateWriteDelay: "500ms",
stateCounterInterval: "1h", stateCounterInterval: "1h",
cacheDir: "/srv/waf-cache",
metricsToken: token, metricsToken: token,
metricsTopN: "10", metricsTopN: "10",
rulesDir: "/srv/waf-rules", rulesDir: "/srv/waf-rules",
@@ -371,7 +368,6 @@ func TestValuesAsSet(t *testing.T) {
StateDir: "/srv/waf-state", StateDir: "/srv/waf-state",
StateWriteDelay: 500 * time.Millisecond, StateWriteDelay: 500 * time.Millisecond,
StateCounterInterval: time.Hour, StateCounterInterval: time.Hour,
CacheDir: "/srv/waf-cache",
MetricsToken: token, MetricsToken: token,
MetricsTopN: 10, MetricsTopN: 10,
RulesDir: "/srv/waf-rules", RulesDir: "/srv/waf-rules",
@@ -2306,7 +2302,6 @@ func TestInvalidBanOrStateValueStopsTheStart(t *testing.T) {
{stateDir, ""}, {stateDir, "state"}, {stateDir, "./var/lib/sww"}, {stateDir, ""}, {stateDir, "state"}, {stateDir, "./var/lib/sww"},
{stateWriteDelay, off}, {stateWriteDelay, "0s"}, {stateWriteDelay, off}, {stateWriteDelay, "0s"},
{stateCounterInterval, off}, {stateCounterInterval, "15"}, {stateCounterInterval, off}, {stateCounterInterval, "15"},
{cacheDir, ""}, {cacheDir, "cache"}, {cacheDir, "./var/cache/sww"},
}) })
} }
@@ -2579,7 +2574,6 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
stateDir: "/var/lib/sww", stateDir: "/var/lib/sww",
stateWriteDelay: "10s", stateWriteDelay: "10s",
stateCounterInterval: "15m", stateCounterInterval: "15m",
cacheDir: "/var/cache/sww",
adminToken: "", adminToken: "",
metricsToken: "", metricsToken: "",
metricsTopN: "50", metricsTopN: "50",
@@ -2680,8 +2674,8 @@ func wantLookupSettings(t *testing.T, got *config.Config, want config.Config) {
} }
} }
// wantBanSettings checks the settings for bans, the state and cache // wantBanSettings checks the settings for bans, the state files, the
// files, the metrics and the rule files. // metrics and the rule files.
func wantBanSettings(t *testing.T, got *config.Config, want config.Config) { func wantBanSettings(t *testing.T, got *config.Config, want config.Config) {
t.Helper() t.Helper()
@@ -2700,7 +2694,11 @@ func wantBanSettings(t *testing.T, got *config.Config, want config.Config) {
got.RulesDir, got.RulesEnabled, want.RulesDir, want.RulesEnabled) got.RulesDir, got.RulesEnabled, want.RulesDir, want.RulesEnabled)
} }
wantStateSettings(t, got, want) if got.StateDir != want.StateDir ||
got.StateWriteDelay != want.StateWriteDelay ||
got.StateCounterInterval != want.StateCounterInterval {
t.Errorf("state settings\n%+v\nwant\n%+v", got, want)
}
if got.MetricsToken != want.MetricsToken || got.MetricsTopN != want.MetricsTopN { if got.MetricsToken != want.MetricsToken || got.MetricsTopN != want.MetricsTopN {
t.Errorf("metrics token %q and top %d, want %q and %d", t.Errorf("metrics token %q and top %d, want %q and %d",
@@ -2708,18 +2706,6 @@ func wantBanSettings(t *testing.T, got *config.Config, want config.Config) {
} }
} }
// wantStateSettings checks the settings for the state and cache files.
func wantStateSettings(t *testing.T, got *config.Config, want config.Config) {
t.Helper()
if got.StateDir != want.StateDir ||
got.StateWriteDelay != want.StateWriteDelay ||
got.StateCounterInterval != want.StateCounterInterval ||
got.CacheDir != want.CacheDir {
t.Errorf("state settings\n%+v\nwant\n%+v", got, want)
}
}
// wantNetblocks checks a list of netblocks. // wantNetblocks checks a list of netblocks.
func wantNetblocks(t *testing.T, got []netip.Prefix, want ...string) { func wantNetblocks(t *testing.T, got []netip.Prefix, want ...string) {
t.Helper() t.Helper()
+18 -7
View File
@@ -1,6 +1,6 @@
// Package coreruleset runs the OWASP Core Rule Set 4.25.0, through Coraza, on the // Package coreruleset runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
// method, the URL with its query and the headers of a request, and on its // method, the URL with its query and the headers of a request, and on its
// body while SWW_WAF_BODY_LIMIT is set, with the fourteen changes sww // body while SWW_WAF_BODY_LIMIT is set, with the fifteen changes sww
// makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as // makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as
// "Attack detection" under "Settings" in SPEC.md describes them. It reads no // "Attack detection" under "Settings" in SPEC.md describes them. It reads no
// response. // response.
@@ -36,7 +36,7 @@ import (
// giteaExclusions for the third, wordpressExclusions for the fourth, // giteaExclusions for the third, wordpressExclusions for the fourth,
// drupalExclusions for the fifth, mediawikiExclusions for the sixth, // drupalExclusions for the fifth, mediawikiExclusions for the sixth,
// mastodonExclusions for the seventh, vaultwardenExclusions for the eighth, // mastodonExclusions for the seventh, vaultwardenExclusions for the eighth,
// and nextcloudExclusions for the ninth. // nextcloudExclusions for the ninth, and forgejoExclusions for the tenth.
// Each rule sww adds has an id from 900000 to 900999, the ids the // Each rule sww adds has an id from 900000 to 900999, the ids the
// Core Rule Set keeps for the rules that set it up, which // Core Rule Set keeps for the rules that set it up, which
// SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no // SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
@@ -102,7 +102,10 @@ SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
# So does the thirteenth. # So does the thirteenth.
%s %s
# And the fourteenth. # So does the fourteenth.
%s
# And the fifteenth.
%s %s
# The sixth: only the rules for requests are loaded, and no response is # The sixth: only the rules for requests are loaded, and no response is
@@ -239,11 +242,19 @@ var vaultwardenExclusions string
//go:embed nextcloud.conf //go:embed nextcloud.conf
var nextcloudExclusions string var nextcloudExclusions string
// forgejoExclusions are the fifteenth change: the fields of Forgejo's own
// requests that skip the rules that refused them there, where Forgejo differs
// from gitea, as Coraza directives.
//
//go:embed forgejo.conf
var forgejoExclusions string
// cookiesNotRead are the cookies the Core Rule Set reads a request // cookiesNotRead are the cookies the Core Rule Set reads a request
// without, the rest of the fifth change. // without, the rest of the fifth change. flash is Forgejo's name for
// gitea_flash.
// //
//nolint:gochecknoglobals // a constant cannot be a list //nolint:gochecknoglobals // a constant cannot be a list
var cookiesNotRead = []string{"gitea_flash", "redirect_to"} var cookiesNotRead = []string{"gitea_flash", "flash", "redirect_to"}
// Params are what New needs. // Params are what New needs.
type Params struct { type Params struct {
@@ -352,7 +363,7 @@ type CoreRuleSet struct {
bodyLimit int64 bodyLimit int64
} }
// New returns the Core Rule Set with the fourteen changes, at params' // New returns the Core Rule Set with the fifteen changes, at params'
// paranoia level, without the rules it switches off, reading request // paranoia level, without the rules it switches off, reading request
// bodies up to params' limit, and with params' exclusions file loaded // bodies up to params' limit, and with params' exclusions file loaded
// before the Core Rule Set's rules. An exclusions file is an error if it // before the Core Rule Set's rules. An exclusions file is an error if it
@@ -382,7 +393,7 @@ func New(params Params) (*CoreRuleSet, error) {
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions, text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
giteaExclusions, wordpressExclusions, drupalExclusions, giteaExclusions, wordpressExclusions, drupalExclusions,
mediawikiExclusions, mastodonExclusions, vaultwardenExclusions, mediawikiExclusions, mastodonExclusions, vaultwardenExclusions,
nextcloudExclusions) nextcloudExclusions, forgejoExclusions)
if len(params.DisabledRules) > 0 { if len(params.DisabledRules) > 0 {
ids := make([]string, len(params.DisabledRules)) ids := make([]string, len(params.DisabledRules))
+8 -4
View File
@@ -204,7 +204,7 @@ func atDefaults(t *testing.T) *coreruleset.CoreRuleSet {
} }
// wantChange checks that crs lets through passes, a request one of the // wantChange checks that crs lets through passes, a request one of the
// fourteen changes is for, and still finds result in refused, a request like // fifteen changes is for, and still finds result in refused, a request like
// it that the change is not for. // it that the change is not for.
func wantChange( func wantChange(
t *testing.T, crs *coreruleset.CoreRuleSet, passes, refused request, t *testing.T, crs *coreruleset.CoreRuleSet, passes, refused request,
@@ -370,7 +370,7 @@ func TestParameterNamesAreMatchedWithoutRegardToCase(t *testing.T) {
get("/?next=http://127.0.0.1:52341/"), matched(931100, 934110)) get("/?next=http://127.0.0.1:52341/"), matched(931100, 934110))
} }
func TestCookiesGiteaFlashAndRedirectToAreNotRead(t *testing.T) { func TestFlashAndRedirectToCookiesAreNotRead(t *testing.T) {
t.Parallel() t.Parallel()
const ( const (
@@ -380,8 +380,12 @@ func TestCookiesGiteaFlashAndRedirectToAreNotRead(t *testing.T) {
crs := atDefaults(t) crs := atDefaults(t)
wantChange(t, crs, get("/owner/repo", "Cookie: gitea_flash="+flash), // flash is Forgejo's name for gitea_flash.
get("/owner/repo", "Cookie: flash="+flash), matched(930120)) for _, name := range []string{"gitea_flash", "flash"} {
wantChange(t, crs, get("/owner/repo", "Cookie: "+name+"="+flash),
get("/owner/repo", "Cookie: message="+flash), matched(930120))
}
wantChange(t, crs, get("/", "Cookie: redirect_to="+redirectTo), wantChange(t, crs, get("/", "Cookie: redirect_to="+redirectTo),
get("/", "Cookie: redirect="+redirectTo), matched(930120)) get("/", "Cookie: redirect="+redirectTo), matched(930120))
+21
View File
@@ -0,0 +1,21 @@
# The fifteenth change: on Forgejo's paths, some fields skip the rules that
# refused ordinary Forgejo requests there. Forgejo is a fork of gitea, and the
# eighth change's fixes for gitea apply to its requests too, so this file holds
# only what Forgejo sends and gitea does not. Each exclusion runs with the
# request, before the rules it switches off, so this file is loaded before the
# Core Rule Set's rules. Each comment names a request the rule refused.
# Forgejo's search of a wiki sends the text searched for in q, which skips
# these rules there:
# 930120: .htaccess
# 932120: powershell
# 932160: /bin/sh
# 932250: ls -la
# 932260: python3
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/wiki/search$" \
"id:900680,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932160;ARGS:q,\
ctl:ruleRemoveTargetById=932250;ARGS:q,\
ctl:ruleRemoveTargetById=932260;ARGS:q"
+78
View File
@@ -0,0 +1,78 @@
package coreruleset_test
import (
"net/http"
"slices"
"testing"
)
// forgejoSearches are Forgejo's searches whose q skips some rules: the
// wiki's, from the fifteenth change, and the searches for a name, from the
// eighth.
//
//nolint:gochecknoglobals // a constant cannot be a list
var forgejoSearches = []string{
"/alice/tools/wiki/search",
"/explore/repos",
"/explore/users",
"/alice/tools/branches",
"/alice/-/packages",
"/api/v1/packages/alice",
"/alice",
}
// forgejoAttacks are an SQL injection, a path traversal, a script injection
// and a shell command after a ";", as query values.
//
//nolint:gochecknoglobals // a constant cannot be a list
var forgejoAttacks = []string{
sqlInjection, "..%2Fcustom%2Fconf%2Fapp.ini", scriptInjection, commandInjection,
}
func TestForgejoAttacksAreRefusedInItsSearches(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
defaults, bodies := atDefaults(t), readingBodies(t)
for _, path := range forgejoSearches {
for _, attack := range forgejoAttacks {
if got := inspect(t, defaults, get(path+"?q="+attack)); got.Score < threshold {
t.Errorf("GET %s?q=%s let through: %+v", path, attack, got)
}
// The same field in a form data body.
r := request{http.MethodPost, path, []string{formData}}
if got, _ := inspectBody(t, bodies, r, "q="+attack); got.Score < threshold {
t.Errorf("POST %s q=%s let through: %+v", path, attack, got)
}
}
}
}
func TestForgejoExclusionsAreNarrow(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
crs := atDefaults(t)
// /bin/sh and the hard values, which q lets through on the wiki's
// search, stay refused in q on paths near it and in another field on it.
// /alice/wiki/search would be the code search of a repository named
// wiki, where the eighth change lets q through. Forgejo has no owner
// named api, so /api/wiki/search is not one.
for _, target := range []string{
"/alice/tools/wiki/Home?q=",
"/alice/tools/wiki/search/x?q=",
"/api/wiki/search?q=",
"/alice/tools/wiki/search?other=",
} {
for _, value := range slices.Concat([]string{"%2Fbin%2Fsh"}, hardValues) {
if got := inspect(t, crs, get(target+value)); got.Score < threshold {
t.Errorf("%s%s let through: %+v", target, value, got)
}
}
}
}
+19 -4
View File
@@ -102,13 +102,16 @@ SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\
# name in q, which skips these rules there: # name in q, which skips these rules there:
# 930120: .profile # 930120: .profile
# 932120: powershell-scripts # 932120: powershell-scripts
# 932250: ls -la
# 932260: python3-app # 932260: python3-app
# 944130: org.apache.commons.logging # 944130: org.apache.commons.logging
SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\ SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
[^/]+/-/packages|[^/]+/[^/]+/(?:branches|tags)|api/v1/repos/search)$" \ [^/]+/-/packages|[^/]+/[^/]+/(?:branches|tags)|api/v1/repos/search|\
api/v1/packages/[^/]+)$" \
"id:900602,phase:1,pass,nolog,\ "id:900602,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\ ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\ ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932250;ARGS:q,\
ctl:ruleRemoveTargetById=932260;ARGS:q,\ ctl:ruleRemoveTargetById=932260;ARGS:q,\
ctl:ruleRemoveTargetById=944130;ARGS:q" ctl:ruleRemoveTargetById=944130;ARGS:q"
@@ -116,19 +119,31 @@ SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
# rules there: # rules there:
# 930120: .profile # 930120: .profile
# 932120: powershell-scripts # 932120: powershell-scripts
# 932250: ls -la
# 932260: python3-app # 932260: python3-app
# The searches: # The searches:
# - the dashboard's filter of repositories; # - the dashboard's filter of repositories;
# - the repositories on a user's or an organisation's page, whose path has
# one segment;
# - the author filter of the issue and pull request lists; # - the author filter of the issue and pull request lists;
# - the lists of users and organisations, and the API's search for users. # - the lists of users and organisations, and the API's search for users.
SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+|repo/search|\ SecRule REQUEST_FILENAME "@rx ^/(?:repo/search|\
explore/(?:users|organizations)|api/v1/users/search|\ explore/(?:users|organizations)|api/v1/users/search|\
[^/]+/[^/]+/(?:issues|pulls)/posters)$" \ [^/]+/[^/]+/(?:issues|pulls)/posters)$" \
"id:900603,phase:1,pass,nolog,\ "id:900603,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\ ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\ ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932250;ARGS:q,\
ctl:ruleRemoveTargetById=932260;ARGS:q"
# The repositories on a user's or an organisation's page are searched with q
# too. Their path has one segment, as other apps' pages such as /index.php do,
# so q keeps 932250 there and skips only these rules:
# 930120: .profile
# 932120: powershell-scripts
# 932260: python3-app
SecRule REQUEST_FILENAME "@rx ^/[^/]+$" \
"id:900604,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932260;ARGS:q" ctl:ruleRemoveTargetById=932260;ARGS:q"
# These requests name a file in the path, which skips these rules there: # These requests name a file in the path, which skips these rules there:
+19
View File
@@ -149,6 +149,25 @@ func TestOrdinaryDrupalRequestsPass(t *testing.T) {
wantRecordedRequestsPass(t, "drupal-requests.txt") wantRecordedRequestsPass(t, "drupal-requests.txt")
} }
// TestOrdinaryForgejoRequestsPass checks Forgejo's recorded requests at the
// defaults only. While bodies are read, the Core Rule Set still refuses the
// file and branch names some of them carry in a form or JSON body, as it does
// gitea's (TestGiteaBodiesTheCoreRuleSetRefuses).
func TestOrdinaryForgejoRequestsPass(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
crs := atDefaults(t)
for _, recorded := range recordedRequests(t, "forgejo-requests.txt") {
r := request{recorded.Method, recorded.Target, recorded.Headers}
if got, _ := inspectBody(t, crs, r, recorded.Body); got.Score >= threshold {
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
}
}
}
func TestOrdinaryMastodonRequestsPass(t *testing.T) { func TestOrdinaryMastodonRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
+470
View File
@@ -0,0 +1,470 @@
# Ordinary Forgejo 17.0.1 requests that the Core Rule Set must let through at
# the defaults. They were recorded from a Forgejo driven by a headless Chrome,
# signed out, signed in and on the admin pages, by git over HTTP, by curl on
# the API and the generic package registry, and by crane on the container
# registry. Each line is one request as JSON. Names, addresses and passwords
# are invented. The browser's User-Agent is an ordinary Chrome's. A body or a
# multipart part that is not text is written as base64.
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/js/webcomponents.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/css/index.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/css/theme-forgejo-auto.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/js/index.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/img/logo.svg", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/explore/repos", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/explore/users", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/explore/users", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/explore/organizations", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/user/forgot_password", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/user/sign_up", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "POST", "target": "/user/sign_up", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 91", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/sign_up", "Accept-Encoding: gzip, deflate"], "body": "user_name=bob&email=bob%40example.com&password=Wd7-kPq3-Zx8n-Rf2m&retype=Wd7-kPq3-Zx8n-Rf2m"}
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/sign_up", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US; flash=success%3DAccount%2Bwas%2Bsuccessfully%2Bcreated.%2BWelcome%2521"], "body": ""}
{"method": "GET", "target": "/assets/js/eventsource.sharedworker.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "Accept: */*", "Referer: http://example.com/", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/css/dashboard-repo-list.98057021.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/dashboard-repo-list.09995173.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/4b9bb80620f03eb3719e0a061c14283d?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?count_only=1&uid=2&team_id=undefined&q=&page=1&mode=", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/events", "headers": ["Host: example.com", "Connection: keep-alive", "Pragma: no-cache", "Cache-Control: no-cache", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/event-stream", "Referer: http://example.com/assets/js/eventsource.sharedworker.js?v=17.0.1~gitea-1.22.0", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=2&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: redirect_to=%2Frepo%2Fsearch%3Fsort%3Dupdated%26order%3Ddesc%26uid%3D2%26team_id%3Dundefined%26q%3D%26page%3D1%26limit%3D15%26mode%3D%26archived%3Dfalse"], "body": ""}
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 43", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate", "Cookie: redirect_to=%2Frepo%2Fsearch%3Fsort%3Dupdated%26order%3Ddesc%26uid%3D2%26team_id%3Dundefined%26q%3D%26page%3D1%26limit%3D15%26mode%3D%26archived%3Dfalse"], "body": "user_name=alice&password=Hv4-mQz8-Lr2x-Tp9w"}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=2&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate", "Cookie: session=cf1a51e0caee441a; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/create", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=cf1a51e0caee441a; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/repo/create", "Accept-Encoding: gzip, deflate", "Cookie: session=cf1a51e0caee441a; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/webcomponents.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/css/index.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/js/index.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/css/theme-forgejo-auto.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/img/logo.svg", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?count_only=1&uid=1&team_id=undefined&q=&page=1&mode=", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
{"method": "POST", "target": "/repo/create", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 179", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/repo/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": "uid=1&repo_name=tools&description=Build+scripts+and+notes&repo_template=&auto_init=on&gitignores=&license=&readme=Default&issue_labels=&object_format_name=sha1&default_branch=main"}
{"method": "GET", "target": "/alice/tools", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/repo/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/css/repo-branch-tag-selector.d9ca1da2.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/repo-branch-tag-selector.f396185d.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/create", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "POST", "target": "/org/create", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 59", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/org/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": "org_name=acme&visibility=0&repo_admin_change_team_access=on"}
{"method": "GET", "target": "/org/acme/dashboard", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/org/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/org/acme/dashboard", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?count_only=1&uid=3&team_id=undefined&q=&page=1&mode=", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/org/acme/dashboard", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=3&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/org/acme/dashboard", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools.git/info/refs?service=git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache", "Git-Protocol: version=2"], "body": ""}
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 181"], "body": "0014command=ls-refs\n001aagent=git/2.49.1-Linux0016object-format=sha100010009peel\n000csymrefs\n000bunborn\n001bref-prefix refs/heads/\n001aref-prefix refs/tags/\n0014ref-prefix HEAD\n0000"}
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 223"], "body": "0011command=fetch001aagent=git/2.49.1-Linux0016object-format=sha10001000dthin-pack000fno-progress000dofs-delta0032want 025ecb5715ad88750bc8944ae3ee0023776b08a3\n0032want 025ecb5715ad88750bc8944ae3ee0023776b08a3\n0009done\n0000"}
{"method": "GET", "target": "/alice/tools.git/info/refs?service=git-receive-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache"], "body": ""}
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 1195"], "body": "MDBiNTAyNWVjYjU3MTVhZDg4NzUwYmM4OTQ0YWUzZWUwMDIzNzc2YjA4YTMgYmJlNTIyM2E5YTRmODRlN2MzZDcyMDZmNGJkZDA2MjIzOWU4MzA5YiByZWZzL2hlYWRzL21haW4AIHJlcG9ydC1zdGF0dXMtdjIgc2lkZS1iYW5kLTY0ayBxdWlldCBvYmplY3QtZm9ybWF0PXNoYTEgYWdlbnQ9Z2l0LzIuNDkuMS1MaW51eDAwMDBQQUNLAAAAAgAAAA+YDXiclYtbCsIwEAD/c4r8C7J5bLMFEXuUzWaLgVZLmoLHtx7B+RkYmN5UrRTyOCtLiSKEgCPMPnoix7FIRBiT5hSd2bjpq1vwqJIxOeRClBCy0Bgja1AF8CGlIQNxMHz057vZaami9sY/PfTD67boVd7r3bo0xkAOh8Fe4MScda2961+TmUqx+ahLsbu0uvXdfAHWnz/YoRR4nDM0MDAzMVHQS88syUzPyy9KZTiU2lsrbdjSY104jdX5NHPG05wrxw2hqjJKEpOTU4uLGaIm9ecuijp2bVeqB3fZBavC0xMSLaCKglwdXXxd9XJTGIra7y45/9VpvTvn0Xl2Kzsfiq1znAJVFJ1SlJhWEgtS9fmKeemzqGM9rJ0rpsnMS4t74ZzpYmIABApJmXkMS35PtDvsY3r29PzXKw5bNU9ZxpERBzUjJT85O7VINzk/tyC/OFWvMjeHQWd5y3flxTNnR91tzrz0g3Vzj8LbmxCzgIqLGfhebpt28ZJ3sYB3caHp4gO26QUZiyDSxclFmQUlxQxfH11qDNt8SW+e8ule1UMm87rDHz6HqihKZkjJPfl/b6+HvVWbwP4LLHLrGh0WWwIAQ5+D9D54nMvLT0mNz81PKc1JLdbnAgAqqgU4sQF4nPMvKMnMzytW0PXMS0mtSC3mAgA5BQYUNnicSylKTCvhAgAIQAIcrQJ4nDM0MDAzMVFIyU/OTi3STc0rKaosyM/MK2EwlNlx2uGU5d7E5T2We5qlPpa4xEUBAGp2Eai0AXicU1bUT8rM0y/O4EqtSE1WUFJxUOICADrbBTiyAnicK04tKstMTi224lJQKE9NAlEKCpm5iempVgp56Zl5FVwAvLIKdq8CeJwzNDAwMzFRyEosS9TLScxL1wsuKcoEUrkpDDqcykduszK1ZnX7O/2+3DJ3wducWABo9hG0OXicU1YILinKzEvnAgANQQLFqQV4nDM0MDAzMVHIzCsuSczJ0S1OLsosKNErzmA4WsLziFHnpxzTZC5LhjXrhBeuDpcyhCguqCzJyM8z1k3OSE3O1iuoZNjBZ6S0Ws37+qnf5umeeyUa/a+llwEAQ3sf9roBeJxTVtRPyszTL87gSk3OyFfIzCsuSczJycxL5wIAbs0Ivzx4nCsoyswr0VDKyFTS5AIAHBwDnqwCeJwzNDAwMzFRKCjKT04tLtZLzSvTS9ErKWZoZrzwV9ki/dEqV+5di3c4P5qYE/gAAFOgEcS7AnicS0lNzkksSlVIzs8rLlFIzSuzUghKTc4vSrEpLinKzEvXUYDQdtZcAF6zD3MdeaAnWyIMS8cNQ71Ry9eDVrG43w=="}
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 617"], "body": "MDBjMjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgZjdmZmE2MDM5NmQ5OWY2MTMxNWE1YTlhMjI1ZmMzMmViNjIyOGZkYiByZWZzL2hlYWRzL3B5dGhvbjMtbWlncmF0aW9uACByZXBvcnQtc3RhdHVzLXYyIHNpZGUtYmFuZC02NGsgcXVpZXQgb2JqZWN0LWZvcm1hdD1zaGExIGFnZW50PWdpdC8yLjQ5LjEtTGludXgwMDAwUEFDSwAAAAIAAAAElg14nJWNSwoCMRBE9zlF7wVJ0j35gIgewEMk6Q4OzJhhiKK3Nx7B2hS8R1F9F4GKwoGzUAmFinHOMCUO1gyKjrC6yDb6qLa0y6NDzjJZiykmqoHEF2RvtauUmbUbJkpAHbNKz35vO1yXuQic0q8u8k7rtsixtPUMxkfCYCbn4aBH1KDr3Lv8NVK39hLoDbbP+Hug+gLJoT+v+gPNgl/qzUzIUFkPJCiBpNxFCX63QXicO8h0kGkDP6NRbPrloHwvB+O3XZczPxVe7CwzvnPLxAAIFIqLkhlSck/+39vrYW/VJrD/AovcukaHxZYA3WMY768HeJwzNDAwMzFRyMwrLknMydEtTi7KLCjRK85gOFrC84hR56cc02QuS4Y164QXrg6XMoQozs1ML0osSdUrqGTYKb5c7aT9BD1nTdkJnNc8mluFrdOhqgoqSzLy84x1kzNSk7NBanfwGSmtVvO+fuq3ebrnXolG/2vpZQCoeiylOXicKyjKzCvRMNTkAgAQzwK6yhVHjb1hxkRjVbbQfVlakFC62Bw="}
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 475"], "body": "MDBjNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgNGI1ZWMxYWFiNjUyNzJiNjNiNWIxZjM5YmZjMmMxNDY4NGMwNjNhMCByZWZzL2hlYWRzL2ZlYXQvZG9ja2VyLXN1cHBvcnQAIHJlcG9ydC1zdGF0dXMtdjIgc2lkZS1iYW5kLTY0ayBxdWlldCBvYmplY3QtZm9ybWF0PXNoYTEgYWdlbnQ9Z2l0LzIuNDkuMS1MaW51eDAwMDBQQUNLAAAAAgAAAAOVDXiclYtbCsIwEEX/s4r8CzLNswERC25kJjODxdaWEMHlW5fg/TlwDrc3EesIQyoeoOQQaXCclFExhsqOKEQFUFJwZscmr26JJDrnsWDQMUiunrODpIGYIR2lyOihkMF3f2zNTstcxV7wh5t8cN0XOddtvdohl+DHIaZsT3DMHHade5e/TmZitvetPqXpvIj5AnIRP0P5As2CX+rNTMhQWQ8kKIGk3EUJfrdBeJw7yPScaUKQvEt+cnZqUVpmTipD+l/zCRITa+JfuB2Sut6UzLn8RVLLRO9vAGSzEms8eJxzC/L3VUjMKcjMS+UCABjGA9h6SWeBbtf5prUzWzGtbNHU1VZzAQ=="}
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 218"], "body": "MDBiNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgYmJlNTIyM2E5YTRmODRlN2MzZDcyMDZmNGJkZDA2MjIzOWU4MzA5YiByZWZzL3RhZ3MvdjEuMC4wACByZXBvcnQtc3RhdHVzLXYyIHNpZGUtYmFuZC02NGsgcXVpZXQgb2JqZWN0LWZvcm1hdD1zaGExIGFnZW50PWdpdC8yLjQ5LjEtTGludXgwMDAwUEFDSwAAAAIAAAAAAp0IgjvYqOq1EK1qx1yCPP0+0x4="}
{"method": "GET", "target": "/alice/powershell-scripts.git/info/refs?service=git-receive-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache"], "body": ""}
{"method": "POST", "target": "/alice/powershell-scripts.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 397"], "body": "MDBiNTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgZGQxYzIyM2RjZWI1NDQxMjdiNzhjZGMyYTZjZTM1NmY4ZWQ5NGYyZCByZWZzL2hlYWRzL21haW4AIHJlcG9ydC1zdGF0dXMtdjIgc2lkZS1iYW5kLTY0ayBxdWlldCBvYmplY3QtZm9ybWF0PXNoYTEgYWdlbnQ9Z2l0LzIuNDkuMS1MaW51eDAwMDBQQUNLAAAAAgAAAAObCXiclYtRCsIwEAX/c4r9FyTbmO0GRPQoSXzBQGOlrODxrUdwfgYGxjaARNsEbjJ5pCRVYp0DtxCKIkfPUqLXO6O4/LbHutFt6RV0zj9d8cnjteBY13EhntMpKEdROvgdt9fRzfDX5Pqzm/sCJg8twKUCeJwzNDAwMzFRCHJ1dPF11ctNYXDttjkS9vTmppsC4WHfVpXW8TJZuQIA3ioNSjp4nFNWKE4uyiwoKeYCABGaA1YUrPZOD6Y21E0qY7z1OfdB9DOSug=="}
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 212"], "body": "0014command=ls-refs\n001aagent=git/2.49.1-Linux0016object-format=sha100010009peel\n000csymrefs\n000bunborn\n001bref-prefix refs/heads/\n001fref-prefix refs/heads/main\n001aref-prefix refs/tags/\n0014ref-prefix HEAD\n0000"}
{"method": "GET", "target": "/assets/js/activity-heatmap.514f8f16.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/bin/docker-entrypoint", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/src/process.env.d.ts", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/docs/java.lang.String.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/%5Bdraft%5D.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/raw/branch/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/raw/branch/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/blame/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/feat/docker-support/Dockerfile", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/releases", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/activity", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/repo-activity-top-authors-contributors-graph-code-frequency-graph-recent-commits-graph.2afcdb46.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/activity", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/repo-activity-top-authors.0b8b147c.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/activity", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/activity", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/graph", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/compare/main...python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/css/dropzone.5a752d14.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/dropzone.265e21d7.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/tag/v1.0.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/archive/main.zip", "headers": ["user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "accept: */*", "accept-encoding: gzip,deflate,br", "cookie: session=d65379616ffd0971; lang=en-US", "Host: example.com", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/repos?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/users?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/organizations?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=repositories&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice?tab=repositories&q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=python3&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/repos?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/users?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/organizations?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=repositories&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=powershell&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/repos?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/users?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/organizations?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=ls%20-la&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/repos?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/users?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/organizations?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=repositories&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=.htaccess&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/repos?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/users?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/organizations?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=repositories&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=docker-compose.yml&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/repos?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/users?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/organizations?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=repositories&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=install-script.sh&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=%2Fbin%2Fsh&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/search?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/repos?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/code?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues?type=your_repositories&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/branches?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/users?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/explore/organizations?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=repositories&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=release%20notes&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/search?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/codemirror-----------------.850d6e6e.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/codemirror.f4be46b8.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/----------------.de1a9813.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/vendors-node_modules_codemirror_lang-yaml_dist_index_js.4f1c0b18.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 278", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "last_commit=bbe5223a9a4f84e7c3d7206f4bdd062239e8309b&page_has_posted=&tree_path=docker-compose.yml&content=services%3A%0D%0A++web%3A%0D%0A++++image%3A+nginx%3A1.27%0D%0A&commit_summary=Pin+nginx&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"method": "GET", "target": "/alice/tools/src/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 272", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "last_commit=2a8b9144333b5cb8075a74c64b31dbaa7ea02b3c&page_has_posted=&tree_path=docs%2Fpython3-setup.md&content=%23+Setup%0D%0A%0D%0AInstall+the+tools.%0D%0A&commit_summary=Add+setup+notes&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"method": "GET", "target": "/assets/js/---.8673fbd3.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/------.2befb329.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/vendors-node_modules_codemirror_lang-markdown_dist_index_js.eef88d50.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main/docs/python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/_upload/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/upload-file", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 224", "Cache-Control: no-cache", "X-Requested-With: XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4q9JlyMIwlkOQ1ZY", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundary4q9JlyMIwlkOQ1ZY\r\nContent-Disposition: form-data; name=\"file\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n------WebKitFormBoundary4q9JlyMIwlkOQ1ZY--\r\n"}
{"method": "POST", "target": "/alice/tools/_upload/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 229", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_upload/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "tree_path=&files=cde01135-f509-483d-af4a-56d1f754aa2c&files_fullpath%5Bcde01135-f509-483d-af4a-56d1f754aa2c%5D=notes.txt&commit_summary=Add+notes&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"method": "GET", "target": "/alice/tools/src/branch/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_upload/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/labels", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/css/colorpicker.6c3c1b7d.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/labels", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/colorpicker.eb94c3c5.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/labels", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/milestones/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 36", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/milestones/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "title=Version+1.1&deadline=&content="}
{"method": "GET", "target": "/alice/tools/milestones", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/milestones/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US; flash=success%3DThe%2Bmilestone%2B%2522Version%2B1.1%2522%2Bhas%2Bbeen%2Bcreated."], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/issues/attachments", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 5018", "Cache-Control: no-cache", "X-Requested-With: XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZATiKc8Kvm2f9kjJ", "Origin: http://example.com", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundaryZATiKc8Kvm2f9kjJ\r\nContent-Disposition: form-data; name=\"file\"; filename=\"screenshot.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAAEAAAAAoCAIAAADBrGu+AAASpklEQVR4nBXYkZ+0UBTG8YENNthggwk22GCCDSaYYJIkSZIkSZIkSX6SJEmSJEmSJEmSJMnKyMjKysrKyMjIyrzz9hfc7/2ce85z2mw2CBvEDdIGeYOyQd2gbdA3GBvMDdYGe4Ozwd3gbfA3BBvCDdGGeEOyId2QbWBDvqHYUG6oNtQbmg3thm5Dv2HYMG6YNswblg3rhs8Npw3nDV8bvjf8bPjdcNlw3XDb8LfhvuFpw/OGlw2vG7Yb3ja8b9ht+Niw33DYcNyw2QgIAqKAJCALKAKqgCagCxgCpoAlYAs4Aq6AJ+ALBAKhQCQQCyQCqUAmgEAuUAiUApVALdAItAKdQC8wCIwCk8AssAisAp8CJ4GzwJfAt8CPwK/AReAqcBP4E7gLPAk8C7wIvApsBd4E3gV2Ah8Ce4GDwFF4AEQEEVFEEpFFFBFVRBPRRQwRU8QSsUUcEVfEE/FFApFQJBKJRRKRVCQTQSQXKURKkUqkFmlEWpFOpBcZREaRSWQWWURWkU+Rk8hZ5EvkW+RH5FfkInIVuYn8idxFnkSeRV5EXkW2Im8i7yI7kQ+RvchB5Cg+ABKChCghScgSioQqoUnoEoaEKWFJ2BKOhCvhSfgSgUQoEUnEEolEKpFJIJFLFBKlRCVRSzQSrUQn0UsMEqPEJDFLLBKrxKfESeIs8SXxLfEj8StxkbhK3CT+JO4STxLPEi8SrxJbiTeJd4mdxIfEXuIgcZQeABlBRpSRZGQZRUaV0WR0GUPGlLFkbBlHxpXxZHyZQCaUiWRimUQmlclkkMllCplSppKpZRqZVqaT6WUGmVFmkpllFplV5lPmJHOW+ZL5lvmR+ZW5yFxlbjJ/MneZJ5lnmReZV5mtzJvMu8xO5kNmL3OQOcoPgIKgICpICrKCoqAqaAq6gqFgKlgKtoKj4Cp4Cr5CoBAqRAqxQqKQKmQKKOQKhUKpUCnUCo1Cq9Ap9AqDwqgwKcwKi8Kq8KlwUjgrfCl8K/wo/CpcFK4KN4U/hbvCk8KzwovCq8JW4U3hXWGn8KGwVzgoHJUHQEVQEVUkFVlFUVFVNBVdxVAxVSwVW8VRcVU8FV8lUAlVIpVYJVFJVTIVVHKVQqVUqVRqlUalVelUepVBZVSZVGaVRWVV+VQ5qZxVvlS+VX5UflUuKleVm8qfyl3lSeVZ5UXlVWWr8qbyrrJT+VDZqxxUjuoDoCFoiBqShqyhaKgamoauYWiYGpaGreFouBqehq8RaIQakUaskWikGpkGGrlGoVFqVBq1RqPRanQavcagMWpMGrPGorFqfGqcNM4aXxrfGj8avxoXjavGTeNP467xpPGs8aLxqrHVeNN419hpfGjsNQ4aR+0B0BF0RB1JR9ZRdFQdTUfXMXRMHUvH1nF0XB1Px9cJdEKdSCfWSXRSnUwHnVyn0Cl1Kp1ap9FpdTqdXmfQGXUmnVln0Vl1PnVOOmedL51vnR+dX52LzlXnpvOnc9d50nnWedF51dnqvOm86+x0PnT2Ogedo/4AGAgGooFkIBsoBqqBZqAbGAamgWVgGzgGroFn4BsEBqFBZBAbJAapQWaAQW5QGJQGlUFt0Bi0Bp1BbzAYjAaTwWywGKwGnwYng7PBl8G3wY/Br8HF4GpwM/gzuBs8GTwbvBi8GmwN3gzeDXYGHwZ7g4PB0XgATAQT0UQykU0UE9VEM9FNDBPTxDKxTRwT18Qz8U0Ck9AkMolNEpPUJDPBJDcpTEqTyqQ2aUxak86kNxlMRpPJZDZZTFaTT5OTydnky+Tb5Mfk1+RicjW5mfyZ3E2eTJ5NXkxeTbYmbybvJjuTD5O9ycHkaD4AFoKFaCFZyBaKhWqhWegWhoVpYVnYFo6Fa+FZ+BaBRWgRWcQWiUVqkVlgkVsUFqVFZVFbNBatRWfRWwwWo8VkMVssFqvFp8XJ4mzxZfFt8WPxa3GxuFrcLP4s7hZPFs8WLxavFluLN4t3i53Fh8Xe4mBxtB4AG8FGtJFsZBvFRrXRbHQbw8a0sWxsG8fGtfFsfJvAJrSJbGKbxCa1yWywyW0Km9KmsqltGpvWprPpbQab0WaymW0Wm9Xm0+Zkc7b5svm2+bH5tbnYXG1uNn82d5snm2ebF5tXm63Nm827zc7mw2Zvc7A52g+Ag+AgOkgOsoPioDpoDrqD4WA6WA62g+PgOngOvkPgEDpEDrFD4pA6ZA445A6FQ+lQOdQOjUPr0Dn0DoPD6DA5zA6Lw+rw6XByODt8OXw7/Dj8Olwcrg43hz+Hu8OTw7PDi8Orw9bhzeHdYefw4bB3ODgcnQfARXARXSQX2UVxUV00F93FcDFdLBfbxXFxXTwX3yVwCV0il9glcUldMhdccpfCpXSpXGqXxqV16Vx6l8FldJlcZpfFZXX5dDm5nF2+XL5dflx+XS4uV5eby5/L3eXJ5dnlxeXVZevy5vLusnP5cNm7HFyO7gPgIXiIHpKH7KF4qB6ah+5heJgeloft4Xi4Hp6H7xF4hB6RR+yReKQemQceuUfhUXpUHrVH49F6dB69x+Axekwes8fisXp8epw8zh5fHt8ePx6/HhePq8fN48/j7vHk8ezx4vHqsfV483j32Hl8eOw9Dh5H7wHwEXxEH8lH9lF8VB/NR/cxfEwfy8f2cXxcH8/H9wl8Qp/IJ/ZJfFKfzAef3KfwKX0qn9qn8Wl9Op/eZ/AZfSaf2WfxWX0+fU4+Z58vn2+fH59fn4vP1efm8+dz93nyefZ58Xn12fq8+bz77Hw+fPY+B5+j/wAECAFigBQgBygBaoAWoAcYAWaAFWAHOAFugBfgBwQBYUAUEAckAWlAFkBAHlAElAFVQB3QBLQBXUAfMASMAVPAHLAErAGfAaeAc8BXwHfAT8BvwCXgGnAL+Au4BzwFPAe8BLwGbAPeAt4DdgEfAfuAQ8AxeABChBAxRAqRQ5QQNUQL0UOMEDPECrFDnBA3xAvxQ4KQMCQKiUOSkDQkCyEkDylCypAqpA5pQtqQLqQPGULGkClkDllC1pDPkFPIOeQr5DvkJ+Q35BJyDbmF/IXcQ55CnkNeQl5DtiFvIe8hu5CPkH3IIeQYPgARQoQYIUXIEUqEGqFF6BFGhBlhRdgRToQb4UX4EUFEGBFFxBFJRBqRRRCRRxQRZUQVUUc0EW1EF9FHDBFjxBQxRywRa8RnxCniHPEV8R3xE/EbcYm4Rtwi/iLuEU8RzxEvEa8R24i3iPeIXcRHxD7iEHGMHoAYIUaMkWLkGCVGjdFi9BgjxoyxYuwYJ8aN8WL8mCAmjIli4pgkJo3JYojJY4qYMqaKqWOamDami+ljhpgxZoqZY5aYNeYz5hRzjvmK+Y75ifmNucRcY24xfzH3mKeY55iXmNeYbcxbzHvMLuYjZh9ziDnGD0CCkCAmSAlygpKgJmgJeoKRYCZYCXaCk+AmeAl+QpAQJkQJcUKSkCZkCSTkCUVCmVAl1AlNQpvQJfQJQ8KYMCXMCUvCmvCZcEo4J3wlfCf8JPwmXBKuCbeEv4R7wlPCc8JLwmvCNuEt4T1hl/CRsE84JByTByBFSBFTpBQ5RUlRU7QUPcVIMVOsFDvFSXFTvBQ/JUgJU6KUOCVJSVOyFFLylCKlTKlS6pQmpU3pUvqUIWVMmVLmlCVlTflMOaWcU75SvlN+Un5TLinXlFvKX8o95SnlOeUl5TVlm/KW8p6yS/lI2accUo7pA5AhZIgZUoacoWSoGVqGnmFkmBlWhp3hZLgZXoafEWSEGVFGnJFkpBlZBhl5RpFRZlQZdUaT0WZ0GX3GkDFmTBlzxpKxZnxmnDLOGV8Z3xk/Gb8Zl4xrxi3jL+Oe8ZTxnPGS8ZqxzXjLeM/YZXxk7DMOGcfsAfj/QwTx/1L+WGsfi+FjtXosJ494/wjIj4j5CGmPmPMICo9R+xhWj3b/aJiPlvN4tI+yfxTO4+ofh///5VBACRXU0EALHfQwwAgTzLDACp9wgjN8wTf8wC9c4Ao3+IM7PMEzvMArbOEN3mEHH7CHAxx5AHKEHDFHypFzlBw1R8vRc4wcM8fKsXOcHDfHy/FzgpwwJ8qJc5KcNCfL/x8/zylyypwqp85pctqcLqfPGXLGnClnzlly1pzPnFPOOecr5zvnJ+c355Jzzbnl/OXcc55ynnNecl5ztjlvOe85u5yPnH3OIeeYPwAFQoFYIBXIBUqBWqLine truncated
{"method": "POST", "target": "/alice/tools/issues/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 1281", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarypBVS0yCpwcYAG4g1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nBuild fails on a clean checkout\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nThe build stops after the first step. It works on the second try.\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"files\"\r\n\r\n179d62f5-4ae4-4e5e-9893-269ae399ca12\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"files_fullpath[179d62f5-4ae4-4e5e-9893-269ae399ca12]\"\r\n\r\nscreenshot.png\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"ref\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"edit_mode\"\r\n\r\ntrue\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"search\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"label_ids\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"milestone_id\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"project_id\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"assignee_ids\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1--\r\n"}
{"method": "POST", "target": "/-/fetch-redirect", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 38", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "redirect=%2Falice%2Ftools%2Fissues%2F1"}
{"method": "GET", "target": "/alice/tools/issues/1", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/1/content-history/overview", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/attachments/179d62f5-4ae4-4e5e-9893-269ae399ca12", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/1", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/issues/1/comments", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 163", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuLA6czyWTpPLGhxN", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundaryuLA6czyWTpPLGhxN\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nThanks, I can see it too.\r\n------WebKitFormBoundaryuLA6czyWTpPLGhxN--\r\n"}
{"method": "POST", "target": "/-/fetch-redirect", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 55", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "redirect=%2Falice%2Ftools%2Fissues%2F1%23issuecomment-1"}
{"method": "GET", "target": "/alice/tools/issues/1", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/releases/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/releases/attachments", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 227", "Cache-Control: no-cache", "X-Requested-With: XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarygH3DxdI69SJCwZ2b", "Origin: http://example.com", "Referer: http://example.com/alice/tools/releases/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundarygH3DxdI69SJCwZ2b\r\nContent-Disposition: form-data; name=\"file\"; filename=\"tools-linux-amd64.tar.gz\"\r\nContent-Type: application/gzip\r\n\r\neJwryc/PKVZIysxLLKosGQFsAIkDYLk=\r\n------WebKitFormBoundarygH3DxdI69SJCwZ2b--\r\n"}
{"method": "POST", "target": "/alice/tools/releases/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 261", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/releases/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "tag_name=v1.1.0&tag_target=main&title=Version+1.1.0&content=Faster+builds.&attachment-template-new-name=&attachment-template-new-exturl=&files=a637a7ba-b419-4ec3-8c33-1c119e93bc14&files_fullpath%5Ba637a7ba-b419-4ec3-8c33-1c119e93bc14%5D=tools-linux-amd64.tar.gz"}
{"method": "GET", "target": "/alice/tools/releases", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/releases/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/?action=_new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/wiki/?action=_new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 78", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "title=Getting+started&content=Clone+the+repository+and+run+the+build.&message="}
{"method": "GET", "target": "/alice/tools/wiki/Getting-started", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/Getting-started", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/?action=_pages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/user/settings/avatar", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 4377", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryR5Mv0yttLbvaY13B", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundaryR5Mv0yttLbvaY13B\r\nContent-Disposition: form-data; name=\"avatar\"; filename=\"avatar.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAQJ0lEQVR4nBXXrbpFUBSG0V0URVEURVEURVEURVEURVEURXmLoiiKoiiKoiiKoiiKoiiKoriJ47iC8Vjz55u/3w/hh/hD+iH/UH6oP7Qf+g/jh/nD+mH/cH64P7wf/o/gR/gj+hH/SH6kP7If/Mh/FD/KH9WP+kfzo/3R/eh/DD/GH9OP+cfyY/2x/dh/HD/OH9eP+8fz4/3x+wkIAqKAJCALKAKqgCagCxgCpoAlYAs4Aq6AJ+ALBAKhQCQQCyQCqUAmgEAuUAiUApVALdAItAKdQC8wCIwCk8AssAisApvALnAInAKXwC3wCLzCBxIRREQRSUQWUURUEU1EFzFETBFLxBZxRFwRT8QXCURCkUgkFklEUpFMBJFcpBApRSqRWqQRaUU6kV5kEBlFJpFZZBFZRTaRXeQQOUUukVvkEXnFDyQhSIgSkoQsoUioEpqELmFImBKWhC3hSLgSnoQvEUiEEpFELJFIpBKZBBK5RCFRSlQStUQj0Up0Er3EIDFKTBKzxCKxSmwSu8QhcUpcErfEI/FKH0hGkBFlJBlZRpFRZTQZXcaQMWUsGVvGkXFlPBlfJpAJZSKZWCaRSWUyGWRymUKmlKlkaplGppXpZHqZQWaUmWRmmUVmldlkdplD5pS5ZG6ZR+aVP5CCoCAqSAqygqKgKmgKuoKhYCpYCraCo+AqeAq+QqAQKkQKsUKikCpkCijkCoVCqVAp1AqNQqvQKfQKg8KoMCnMCovCqrAp7AqHwqlwKdwKj8KrfCAVQUVUkVRkFUVFVdFUdBVDxVSxVGwVR8VV8VR8lUAlVIlUYpVEJVXJVFDJVQqVUqVSqVUalValU+lVBpVRZVKZVRaVVWVT2VUOlVPlUrlVHpVX/UAagoaoIWnIGoqGqqFp6BqGhqlhadgajoar4Wn4GoFGqBFpxBqJRqqRaaCRaxQapUalUWs0Gq1Gp9FrDBqjxqQxaywaq8amsWscGqfGpXFrPBqv9oF0BB1RR9KRdRQdVUfT0XUMHVPH0rF1HB1Xx9PxdQKdUCfSiXUSnVQn00En1yl0Sp1Kp9ZpdFqdTqfXGXRGnUln1ll0Vp1NZ9c5dE6dS+fWeXRe/QMZCAaigWQgGygGqoFmoBsYBqaBZWAbOAaugWfgGwQGoUFkEBskBqlBZoBBblAYlAaVQW3QGLQGnUFvMBiMBpPBbLAYrAabwW5wGJwGl8Ft8Bi8xgcyEUxEE8lENlFMVBPNRDcxTEwTy8Q2cUxcE8/ENwlMQpPIJDZJTFKTzAST3KQwKU0qk9qkMWlNOpPeZDAZTSaT2WQxWU02k93kMDlNLpPb5DF5zQ9kIViIFpKFbKFYqBaahW5hWJgWloVt4Vi4Fp6FbxFYhBaRRWyRWKQWmQUWuUVhUVpUFrVFY9FadBa9xWAxWkwWs8VisVpsFrvFYXFaXBa3xWPxWh/IRrARbSQb2UaxUW00G93GsDFtLBvbxrFxbTwb3yawCW0im9gmsUltMhtscpvCprSpbGqbxqa16Wx6m8FmtJlsZpvFZrXZbHabw+a0uWxum8fmtT+Qg+AgOkgOsoPioDpoDrqD4WA6WA62g+PgOngOvkPgEDpEDrFD4pA6ZA445A6FQ+lQOdQOjUPr0Dn0DoPD6DA5zA6Lw+qwOewOh8PpcDncDo/D63wgF8FFdJFcZBfFRXXRXHQXw8V0sVxsF8fFdfFcfJfAJXSJXGKXxCV1yVxwyV0Kl9KlcqldGpfWpXPpXQaX0WVymV0Wl9Vlc9ldDpfT5XK5XR6X1/1AHoKH6CF5yB6Kh+qheegehofpYXnYHo6H6+F5+B6BR+gRecQeiUfqkXngkXsUHqVH5VF7NB6tR+fRewweo8fkMXssHqvH5rF7HB6nx+Vxezwer/eBfAQf0UfykX0UH9VH89F9DB/Tx/KxfRwf18fz8X0Cn9An8ol9Ep/UJ/PBJ/cpfEqfyqf2aXxan86n9xl8Rp/JZ/ZZfFafzWf3OXxOn8vn9nl8Xv8DBQgBYoAUIAcoAWqAFqAHGAFmgBVgBzgBboAX4AcEAWFAFBAHJAFpQBZAQB5QBJQBVUAd0AS0AV1AHzAEjAFTwBywBKwBW8AecAScAVfAHfAEvMEHChFCxBApRA5RQtQQLUQPMULMECvEDnFC3BAvxA8JQsKQKCQOSULSkCyEkDykCClDqpA6pAlpQ7qQPmQIGUOmkDlkCVlDtpA95Ag5Q66QO+QJecMPFCFEiBFShByhRKgRWoQeYUSYEVaEHeFEuBFehB8RRIQRUUQckUSkEVkEEXlEEVFGVBF1RBPRRnQRfcQQMUZMEXPEErFGbBF7xBFxRlwRd8QT8UYfKEaIEWOkGDlGiVFjtBg9xogxY6wYO8aJcWO8GD8miAljopg4JolJY7IYYvKYIqaMqWLqmCamjeli+pghZoyZYuaYJWaN2WL2mCPmjLli7pgn5o0/UIKQICZICXKCkqAmaAl6gpFgJlgJdoKT4CZ4CX5CkBAmRAlxQpKQJmQJJOQJRUKZUCXUCU1Cm9Al9AlDwpgwJcwJS8KasCXsCUfCmXAl3AlPwpt8oBQhRUyRUuQUJUVN0VL0FCPFTLFS7BQnxU3xUvyUICVMiVLilCQlTclSSMlTipQypUqpU5qUNqVL6VOGlDFlSplTlpQ1ZUvZU46UM+VKuVOelDf9QBlChpghZcgZSoaaoWXoGUaGmWFl2BlOhpvhZfgZQUaYEWXEGUlGmpFlkJFnFBllRpVRZzQZbUaX0WcMGWPGlDFnLBlrxpaxZxwZZ8aVcWc8GW/2gf4PmO9k+EL6F4u/IPpFvy9sffHmCxTfCv+W5remvsXwjeJv+H3j5mvwr6W+Iv7K5nuo79d8mP8vhwJKqKCGBlrooIcBRphghgVW2GCHA0644IYH3u+8++UIOWKOlCPnKDlqjpaj5xg5Zo6VY+c4OW6Ol+PnBDlhTpQT5yQ5aU6W/3PynCKnzKly6pwmp83pcvqcIWfMmXLmnCVnzdly9pwj58y5cu6cJ+fNP1CBUCAWSAVygVKgFmgFeoFRYBZYBXaBU+AWeAV+QVAQFkQFcUFSkBZkxf/PyQuKgrKgKqgLmoK2oCvoC4aCsWAqmAuWgrVgK9gLjoKz4Cq4C56Ct/hAJUKJWCKVyCVKiVqileglRolZYpXYJU6JW+KV+CVBSVgSlcQlSUlakpX/T5WXFCVlSVVSlzQlbUlX0pcMJWPJVDKXLCVryVaylxwlZ8lVcpc8JW/5gSqECrFCqpArlAq1QqvQK4wKs8KqsCucCrfCq/ArgoqwIqqIK5KKtCKr/gsnrygqyoqqoq5oKtqKrqKvGCrGiqlirlgq1oqtYq84Ks6Kq+KueCre6gPVCDVijVQj1yg1ao1Wo9cYNWaNVWPXODVujVfj1wQ1YU1UE9ckNWlNVv+XcV5T1JQ1VU1d09S0NV1NXzPUjDVTzVyz1Kw1W81ec9ScNVfNXfPUvPUHahAaxAapQW5QGtQGrUFvMBrMBqvBbnAa3AavwW8IGsKGqCFuSBrShqz5b6q8oWgoG6qGuqFpaBu6hr5haBgbpoa5YWlYG7aGveFoOBuuhrvhaXibD9QitIgtUovcorSoLVqL3mK0mC1Wi93itLgtXovfErSELVFL3JK0pC1Z+9/ieUvRUrZULXVL09K2dC19y9Aytkwtc8vSsrZsLXvL0XK2XC13y9Pyth+oQ+gQO6QOuUPpUDu0Dr3D6DA7rA67w+lwO7wOvyPoCDuijrgj6Ug7su5/4OQdRUfZUXXUHU1H29F19B1Dx9gxdcwdS8fasXXsHUfH2XF13B1Px9t9oB6hR+yReuQepUft0Xr0HqPH7LF67B6nx+3xevyeoCfsiXrinqQn7cn6//GX9xQ9ZU/VU/c0PW1P19P3DD1jz9Qz9yw9a8/Ws/ccPWfP1XP3PD1v/4EGhAFxQBqQB5QBdUAb0AeMAXPAGrAHnAF3wBvwB4KBcCAaiAeSgXQgG/6HcT5QDJQD1UA90Ay0A91APzAMjAPTwDywDKwD28A+cAycA9fAPfAMvMMHGhFGxBFpRB5RRtQRbUQfMUbMEWvEHnFG3BFvxLine truncated
{"method": "GET", "target": "/user/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US; flash=success%3DYour%2Bavatar%2Bhas%2Bbeen%2Bupdated."], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/settings/avatar", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 4377", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykD1bO7T1SED3rJ5v", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundarykD1bO7T1SED3rJ5v\r\nContent-Disposition: form-data; name=\"avatar\"; filename=\"avatar.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAQJ0lEQVR4nBXXrbpFUBSG0V0URVEURVEURVEURVEURVEURXmLoiiKoiiKoiiKoiiKoiiKoriJ47iC8Vjz55u/3w/hh/hD+iH/UH6oP7Qf+g/jh/nD+mH/cH64P7wf/o/gR/gj+hH/SH6kP7If/Mh/FD/KH9WP+kfzo/3R/eh/DD/GH9OP+cfyY/2x/dh/HD/OH9eP+8fz4/3x+wkIAqKAJCALKAKqgCagCxgCpoAlYAs4Aq6AJ+ALBAKhQCQQCyQCqUAmgEAuUAiUApVALdAItAKdQC8wCIwCk8AssAisApvALnAInAKXwC3wCLzCBxIRREQRSUQWUURUEU1EFzFETBFLxBZxRFwRT8QXCURCkUgkFklEUpFMBJFcpBApRSqRWqQRaUU6kV5kEBlFJpFZZBFZRTaRXeQQOUUukVvkEXnFDyQhSIgSkoQsoUioEpqELmFImBKWhC3hSLgSnoQvEUiEEpFELJFIpBKZBBK5RCFRSlQStUQj0Up0Er3EIDFKTBKzxCKxSmwSu8QhcUpcErfEI/FKH0hGkBFlJBlZRpFRZTQZXcaQMWUsGVvGkXFlPBlfJpAJZSKZWCaRSWUyGWRymUKmlKlkaplGppXpZHqZQWaUmWRmmUVmldlkdplD5pS5ZG6ZR+aVP5CCoCAqSAqygqKgKmgKuoKhYCpYCraCo+AqeAq+QqAQKkQKsUKikCpkCijkCoVCqVAp1AqNQqvQKfQKg8KoMCnMCovCqrAp7AqHwqlwKdwKj8KrfCAVQUVUkVRkFUVFVdFUdBVDxVSxVGwVR8VV8VR8lUAlVIlUYpVEJVXJVFDJVQqVUqVSqVUalValU+lVBpVRZVKZVRaVVWVT2VUOlVPlUrlVHpVX/UAagoaoIWnIGoqGqqFp6BqGhqlhadgajoar4Wn4GoFGqBFpxBqJRqqRaaCRaxQapUalUWs0Gq1Gp9FrDBqjxqQxaywaq8amsWscGqfGpXFrPBqv9oF0BB1RR9KRdRQdVUfT0XUMHVPH0rF1HB1Xx9PxdQKdUCfSiXUSnVQn00En1yl0Sp1Kp9ZpdFqdTqfXGXRGnUln1ll0Vp1NZ9c5dE6dS+fWeXRe/QMZCAaigWQgGygGqoFmoBsYBqaBZWAbOAaugWfgGwQGoUFkEBskBqlBZoBBblAYlAaVQW3QGLQGnUFvMBiMBpPBbLAYrAabwW5wGJwGl8Ft8Bi8xgcyEUxEE8lENlFMVBPNRDcxTEwTy8Q2cUxcE8/ENwlMQpPIJDZJTFKTzAST3KQwKU0qk9qkMWlNOpPeZDAZTSaT2WQxWU02k93kMDlNLpPb5DF5zQ9kIViIFpKFbKFYqBaahW5hWJgWloVt4Vi4Fp6FbxFYhBaRRWyRWKQWmQUWuUVhUVpUFrVFY9FadBa9xWAxWkwWs8VisVpsFrvFYXFaXBa3xWPxWh/IRrARbSQb2UaxUW00G93GsDFtLBvbxrFxbTwb3yawCW0im9gmsUltMhtscpvCprSpbGqbxqa16Wx6m8FmtJlsZpvFZrXZbHabw+a0uWxum8fmtT+Qg+AgOkgOsoPioDpoDrqD4WA6WA62g+PgOngOvkPgEDpEDrFD4pA6ZA445A6FQ+lQOdQOjUPr0Dn0DoPD6DA5zA6Lw+qwOewOh8PpcDncDo/D63wgF8FFdJFcZBfFRXXRXHQXw8V0sVxsF8fFdfFcfJfAJXSJXGKXxCV1yVxwyV0Kl9KlcqldGpfWpXPpXQaX0WVymV0Wl9Vlc9ldDpfT5XK5XR6X1/1AHoKH6CF5yB6Kh+qheegehofpYXnYHo6H6+F5+B6BR+gRecQeiUfqkXngkXsUHqVH5VF7NB6tR+fRewweo8fkMXssHqvH5rF7HB6nx+Vxezwer/eBfAQf0UfykX0UH9VH89F9DB/Tx/KxfRwf18fz8X0Cn9An8ol9Ep/UJ/PBJ/cpfEqfyqf2aXxan86n9xl8Rp/JZ/ZZfFafzWf3OXxOn8vn9nl8Xv8DBQgBYoAUIAcoAWqAFqAHGAFmgBVgBzgBboAX4AcEAWFAFBAHJAFpQBZAQB5QBJQBVUAd0AS0AV1AHzAEjAFTwBywBKwBW8AecAScAVfAHfAEvMEHChFCxBApRA5RQtQQLUQPMULMECvEDnFC3BAvxA8JQsKQKCQOSULSkCyEkDykCClDqpA6pAlpQ7qQPmQIGUOmkDlkCVlDtpA95Ag5Q66QO+QJecMPFCFEiBFShByhRKgRWoQeYUSYEVaEHeFEuBFehB8RRIQRUUQckUSkEVkEEXlEEVFGVBF1RBPRRnQRfcQQMUZMEXPEErFGbBF7xBFxRlwRd8QT8UYfKEaIEWOkGDlGiVFjtBg9xogxY6wYO8aJcWO8GD8miAljopg4JolJY7IYYvKYIqaMqWLqmCamjeli+pghZoyZYuaYJWaN2WL2mCPmjLli7pgn5o0/UIKQICZICXKCkqAmaAl6gpFgJlgJdoKT4CZ4CX5CkBAmRAlxQpKQJmQJJOQJRUKZUCXUCU1Cm9Al9AlDwpgwJcwJS8KasCXsCUfCmXAl3AlPwpt8oBQhRUyRUuQUJUVN0VL0FCPFTLFS7BQnxU3xUvyUICVMiVLilCQlTclSSMlTipQypUqpU5qUNqVL6VOGlDFlSplTlpQ1ZUvZU46UM+VKuVOelDf9QBlChpghZcgZSoaaoWXoGUaGmWFl2BlOhpvhZfgZQUaYEWXEGUlGmpFlkJFnFBllRpVRZzQZbUaX0WcMGWPGlDFnLBlrxpaxZxwZZ8aVcWc8GW/2gf4PmO9k+EL6F4u/IPpFvy9sffHmCxTfCv+W5remvsXwjeJv+H3j5mvwr6W+Iv7K5nuo79d8mP8vhwJKqKCGBlrooIcBRphghgVW2GCHA0644IYH3u+8++UIOWKOlCPnKDlqjpaj5xg5Zo6VY+c4OW6Ol+PnBDlhTpQT5yQ5aU6W/3PynCKnzKly6pwmp83pcvqcIWfMmXLmnCVnzdly9pwj58y5cu6cJ+fNP1CBUCAWSAVygVKgFmgFeoFRYBZYBXaBU+AWeAV+QVAQFkQFcUFSkBZkxf/PyQuKgrKgKqgLmoK2oCvoC4aCsWAqmAuWgrVgK9gLjoKz4Cq4C56Ct/hAJUKJWCKVyCVKiVqileglRolZYpXYJU6JW+KV+CVBSVgSlcQlSUlakpX/T5WXFCVlSVVSlzQlbUlX0pcMJWPJVDKXLCVryVaylxwlZ8lVcpc8JW/5gSqECrFCqpArlAq1QqvQK4wKs8KqsCucCrfCq/ArgoqwIqqIK5KKtCKr/gsnrygqyoqqoq5oKtqKrqKvGCrGiqlirlgq1oqtYq84Ks6Kq+KueCre6gPVCDVijVQj1yg1ao1Wo9cYNWaNVWPXODVujVfj1wQ1YU1UE9ckNWlNVv+XcV5T1JQ1VU1d09S0NV1NXzPUjDVTzVyz1Kw1W81ec9ScNVfNXfPUvPUHahAaxAapQW5QGtQGrUFvMBrMBqvBbnAa3AavwW8IGsKGqCFuSBrShqz5b6q8oWgoG6qGuqFpaBu6hr5haBgbpoa5YWlYG7aGveFoOBuuhrvhaXibD9QitIgtUovcorSoLVqL3mK0mC1Wi93itLgtXovfErSELVFL3JK0pC1Z+9/ieUvRUrZULXVL09K2dC19y9Aytkwtc8vSsrZsLXvL0XK2XC13y9Pyth+oQ+gQO6QOuUPpUDu0Dr3D6DA7rA67w+lwO7wOvyPoCDuijrgj6Ug7su5/4OQdRUfZUXXUHU1H29F19B1Dx9gxdcwdS8fasXXsHUfH2XF13B1Px9t9oB6hR+yReuQepUft0Xr0HqPH7LF67B6nx+3xevyeoCfsiXrinqQn7cn6//GX9xQ9ZU/VU/c0PW1P19P3DD1jz9Qz9yw9a8/Ws/ccPWfP1XP3PD1v/4EGhAFxQBqQB5QBdUAb0AeMAXPAGrAHnAF3wBvwB4KBcCAaiAeSgXQgG/6HcT5QDJQD1UA90Ay0A91APzAMjAPTwDywDKwD28A+cAycA9fAPfAMvMMHGhFGxBFpRB5RRtQRbUQfMUbLine truncated
{"method": "GET", "target": "/alice/tools/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US; flash=success%3DThe%2Brepository%2Bavatar%2Bhas%2Bbeen%2Bupdated."], "body": ""}
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/collaboration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=bob", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/javascript, */*; q=0.01", "X-Requested-With: XMLHttpRequest", "Referer: http://example.com/alice/tools/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/4b9bb80620f03eb3719e0a061c14283d", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=bob", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/applications", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/users", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/users?q=bob", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?q=example.com", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/auths", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/config", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/config/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/monitor/stats", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/monitor/queue", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/monitor/cron", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/hooks", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/gogs.ico", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/feishu.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/slack.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/discord.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/telegram.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/dingtalk.ico", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/msteams.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/wechatwork.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/packagist.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/img/sourcehut.svg", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/notices", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/users/2", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/4b9bb80620f03eb3719e0a061c14283d?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/users/2", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/users/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/account", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/appearance", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/security", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/keys", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/packages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=activity", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice?tab=stars", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/dashboard", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/acme", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/branches", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/hooks", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/units", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/migrate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=b7d8a1e4a0871671; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=b7d8a1e4a0871671; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/applications/tokens/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=890deb73026afc5f; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/compare/main...python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 764", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryaGATihSdXZHYz0ae", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nMove to python3\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nPorts the scripts.\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"label_ids\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"milestone_id\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"project_id\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"assignee_ids\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"redirect_after_creation\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae--\r\n"}
{"method": "POST", "target": "/-/fetch-redirect", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 37", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "redirect=%2Falice%2Ftools%2Fpulls%2F2"}
{"method": "GET", "target": "/alice/tools/pulls/2", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/2/content-history/overview", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/pulls/2", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/css/pull-request-merge-form.08cfd3a0.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/pulls/2", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/pull-request-merge-form.8413f096.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/pulls/2", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/2", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/2/commits", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/2/files", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/css/diff-commit-selector.17df8f29.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/pulls/2/files", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/diff-commit-selector.302a470f.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/pulls/2/files", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "POST", "target": "/user/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 142", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "name=alice&full_name=Alice+Example&pronouns=&biography=Writes+build+tools.&website=https%3A%2F%2Fexample.com&location=Springfield&visibility=0"}
{"method": "POST", "target": "/admin/users/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 185", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/new", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "_autofill_dummy_username=&_autofill_dummy_password=&login_type=0-0&visibility=0&login_name=&user_name=carol&email=carol%40example.com&password=Qe5-tVb7-Mn3c-Kj8y&must_change_password=on"}
{"method": "GET", "target": "/admin/users/4", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/new", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US; flash=success%3DThe%2Buser%2Baccount%2B%2522carol%2522%2Bhas%2Bbeen%2Bcreated."], "body": ""}
{"method": "GET", "target": "/avatars/d4766e3f21c67b7c786f012d910fa54f?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/users/4", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/users/2/edit", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/users/2/edit", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 265", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/2/edit", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "_autofill_dummy_username=&_autofill_dummy_password=&user_name=bob&login_type=0-0&visibility=0&login_name=&full_name=Bob+Example&pronouns=&email=bob%40example.com&password=&language=en-US&website=&location=&max_repo_creation=-1&active=on&allow_create_organization=on"}
{"method": "GET", "target": "/admin/users/2", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/2/edit", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US; flash=success%3DThe%2Buser%2Baccount%2Bhas%2Bbeen%2Bupdated."], "body": ""}
{"method": "POST", "target": "/user/settings/applications/tokens/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 241", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings/applications/tokens/new", "Accept-Encoding: gzip, deflate", "Cookie: session=34fdf904aa90270d; lang=en-US"], "body": "name=build-token&repo_search=&page=1&scope=write%3Aactivitypub&scope=write%3Aadmin&scope=write%3Aissue&scope=write%3Amisc&scope=write%3Anotification&scope=write%3Aorganization&scope=write%3Apackage&scope=write%3Arepository&scope=write%3Auser"}
{"method": "POST", "target": "/user/settings/applications/tokens/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 254", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings/applications/tokens/new", "Accept-Encoding: gzip, deflate", "Cookie: session=8ed5d13514629cf4; lang=en-US"], "body": "name=build-token&resource=all&repo_search=&page=1&scope=write%3Aactivitypub&scope=write%3Aadmin&scope=write%3Aissue&scope=write%3Amisc&scope=write%3Anotification&scope=write%3Aorganization&scope=write%3Apackage&scope=write%3Arepository&scope=write%3Auser"}
{"method": "GET", "target": "/user/settings/applications", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings/applications/tokens/new", "Accept-Encoding: gzip, deflate", "Cookie: session=8ed5d13514629cf4; lang=en-US; flash=info%3D518efde244d57864ef12487cdec73aa828e5deaf%26success%3DYour%2Bnew%2Btoken%2Bhas%2Bbeen%2Bgenerated.%2BCopy%2Bit%2Bnow%2Bas%2Bit%2Bwill%2Bnot%2Bbe%2Bshown%2Bagain."], "body": ""}
{"method": "GET", "target": "/api/v1/version", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/user", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/settings/api", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/search?q=python3&limit=10", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/search?q=powershell", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/search?q=.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/users/search?q=bob", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/users/search?q=python3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/issues/search?q=ls%20-la&type=issues", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/issues/search?q=build", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/issues?state=open&q=python3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/issues?state=all&q=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/branches/python3-migration", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/tags", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents?ref=main", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents/.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents/docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents/scripts/install-script.sh?ref=main", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/raw/bin/docker-entrypoint", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/media/src/process.env.d.ts", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/commits?path=scripts/install-script.sh&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/commits?sha=python3-migration", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/git/trees/main", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/labels", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/milestones", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/collaborators", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/hooks", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/admin/users", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/admin/orgs", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/orgs/acme", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/users/alice/repos", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/notifications", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "POST", "target": "/api/v1/repos/alice/tools/labels", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 68"], "body": "{\"name\":\"bug\",\"color\":\"#ee0701\",\"description\":\"Something is broken\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 83"], "body": "{\"title\":\"Document the python3 setup\",\"body\":\"Say which version the scripts need.\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues/3/comments", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 40"], "body": "{\"body\":\"The setup notes cover it now.\"}"}
{"method": "PATCH", "target": "/api/v1/repos/alice/tools/issues/3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 18"], "body": "{\"state\":\"closed\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues/3/assets", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------c8PTgaGzJM0bzMXgzs4jFA"], "body": "--------------------------c8PTgaGzJM0bzMXgzs4jFA\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------c8PTgaGzJM0bzMXgzs4jFA--\r\n"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues/3/assets?name=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------8vBtFvaNGea9428ByqN6zh"], "body": "--------------------------8vBtFvaNGea9428ByqN6zh\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------8vBtFvaNGea9428ByqN6zh--\r\n"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/releases", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 69"], "body": "{\"tag_name\":\"v1.2.0\",\"name\":\"Version 1.2.0\",\"body\":\"Smaller images.\"}"}
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases/tags/v1.2.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "POST", "target": "/api/v1/repos/alice/tools/releases/3/assets?name=install-script.sh", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------a8C2mRAVkCsfebNVoTuH9d"], "body": "--------------------------a8C2mRAVkCsfebNVoTuH9d\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------a8C2mRAVkCsfebNVoTuH9d--\r\n"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/releases/3/assets", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------bwR0eGsF72IMLlKpnzjarK"], "body": "--------------------------bwR0eGsF72IMLlKpnzjarK\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------bwR0eGsF72IMLlKpnzjarK--\r\n"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/scripts/powershell-build.ps1", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 100"], "body": "{\"content\":\"V3JpdGUtSG9zdCAiYnVpbGQiCg==\",\"message\":\"Add a PowerShell build script\",\"branch\":\"main\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 59"], "body": "{\"new_branch_name\":\"python3-docs\",\"old_branch_name\":\"main\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 96"], "body": "{\"title\":\"Add Dockerfile\",\"head\":\"feat/docker-support\",\"base\":\"main\",\"body\":\"Builds the image.\"}"}
{"method": "PATCH", "target": "/api/v1/repos/alice/tools", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 87"], "body": "{\"description\":\"Build scripts, notes and a Dockerfile\",\"website\":\"https://example.com\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/forks", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 23"], "body": "{\"organization\":\"acme\"}"}
{"method": "POST", "target": "/api/v1/admin/users", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 107"], "body": "{\"username\":\"dave\",\"email\":\"dave@example.com\",\"password\":\"Lp6-xRw4-Bq9z-Hs3d\",\"must_change_password\":false}"}
{"method": "PATCH", "target": "/api/v1/admin/users/dave", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 80"], "body": "{\"login_name\":\"dave\",\"full_name\":\"Dave Example\",\"password\":\"Yt2-cNm8-Fg5k-Wa7e\"}"}
{"method": "DELETE", "target": "/api/v1/repos/alice/tools/branches/python3-docs", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/generic/tools/1.2.0/tools-linux-amd64.tar.gz", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/packages/alice?q=tools", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "GET", "target": "/api/v1/packages/alice?type=generic&q=install-script", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
{"method": "PUT", "target": "/api/packages/alice/generic/tools/1.2.0/tools-linux-amd64.tar.gz", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: curl/8.17.0", "Accept: */*", "Content-Length: 41"], "body": "Steps to reproduce: run the build twice.\n"}
{"method": "PUT", "target": "/api/packages/alice/generic/install-script/1.0.0/install-script.sh", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: curl/8.17.0", "Accept: */*", "Content-Length: 41"], "body": "Steps to reproduce: run the build twice.\n"}
{"method": "GET", "target": "/v2/", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fpython3-app%3Apush%2Cpull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
{"method": "HEAD", "target": "/v2/alice/python3-app/manifests/1.0", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
{"method": "HEAD", "target": "/v2/alice/python3-app/blobs/sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
{"method": "POST", "target": "/v2/alice/python3-app/blobs/uploads/", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/json", "Accept-Encoding: gzip"], "body": ""}
{"method": "HEAD", "target": "/v2/alice/python3-app/blobs/sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
{"method": "PATCH", "target": "/v2/alice/python3-app/blobs/uploads/iljdbd0gxw2zdibpfrajzu", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 144", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": "H4sIAAAAAAAE/+zRQQoCIRTG8bfuFJ6g+Wu+8TwughYDymTQ8YOaNhK0More+y1E3ej35VonGTwASVV4TL8CiNeQFB9jQvAcohenQ1+1uZxbXp2TtZS2Hb307v5H5Vqn03FZyr5dR/0PYI6x7/25B+au/xDw4vhEpn/e/7373fiYjTHGfJfbAH2FcNwACgAA"}
{"method": "PUT", "target": "/v2/alice/python3-app/blobs/uploads/iljdbd0gxw2zdibpfrajzu?digest=sha256%3A1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": ""}
{"method": "PATCH", "target": "/v2/alice/python3-app/blobs/uploads/1eu6yk4t4l8lmbsedmyw9l", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 233", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": "{\"architecture\":\"\",\"created\":\"0001-01-01T00:00:00Z\",\"history\":[{\"created\":\"0001-01-01T00:00:00Z\"}],\"os\":\"\",\"rootfs\":{\"type\":\"layers\",\"diff_ids\":[\"sha256:19e75beae854ceb749acf78607bea3b2c0b04b74bbcc28f15e846683cf8e0b9d\"]},\"config\":{}}"}
{"method": "PUT", "target": "/v2/alice/python3-app/blobs/uploads/1eu6yk4t4l8lmbsedmyw9l?digest=sha256%3Af4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": ""}
{"method": "PUT", "target": "/v2/alice/python3-app/manifests/1.0", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 423", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/vnd.docker.distribution.manifest.v2+json", "Accept-Encoding: gzip"], "body": "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\"config\":{\"mediaType\":\"application/vnd.docker.container.image.v1+json\",\"size\":233,\"digest\":\"sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d\"},\"layers\":[{\"mediaType\":\"application/vnd.docker.image.rootfs.diff.tar.gzip\",\"size\":144,\"digest\":\"sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068\"}]}"}
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fpython3-app%3Apull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/alice/python3-app/manifests/1.0", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fdocker-build%3Apush%2Cpull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
{"method": "HEAD", "target": "/v2/alice/docker-build/manifests/latest", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
{"method": "HEAD", "target": "/v2/alice/docker-build/blobs/sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
{"method": "HEAD", "target": "/v2/alice/docker-build/blobs/sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fdocker-build%3Apush%2Cpull&scope=repository%3Aalice%2Fpython3-app%3Apull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
{"method": "POST", "target": "/v2/alice/docker-build/blobs/uploads/?from=alice%2Fpython3-app&mount=sha256%3Af4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d&origin=example.com", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/json", "Accept-Encoding: gzip"], "body": ""}
{"method": "POST", "target": "/v2/alice/docker-build/blobs/uploads/?from=alice%2Fpython3-app&mount=sha256%3A1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068&origin=example.com", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/json", "Accept-Encoding: gzip"], "body": ""}
{"method": "PUT", "target": "/v2/alice/docker-build/manifests/latest", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 423", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/vnd.docker.distribution.manifest.v2+json", "Accept-Encoding: gzip"], "body": "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\"config\":{\"mediaType\":\"application/vnd.docker.container.image.v1+json\",\"size\":233,\"digest\":\"sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d\"},\"layers\":[{\"mediaType\":\"application/vnd.docker.image.rootfs.diff.tar.gzip\",\"size\":144,\"digest\":\"sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068\"}]}"}
{"method": "GET", "target": "/v2/alice/python3-app/tags/list?n=1000", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/alice/python3-app/tags/list?last=1.0&n=1000", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fdocker-build%3Apull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/alice/docker-build/manifests/latest", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/alice/docker-build/blobs/sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
{"method": "GET", "target": "/v2/alice/docker-build/blobs/sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 473"], "body": "0011command=fetch001aagent=git/2.49.1-Linux0016object-format=sha10001000dthin-pack000fno-progress000dofs-delta0032want 0992a6293c77c3ef1f9a8d37b774deddcbef2e23\n0032want 4b5ec1aab65272b63b5b1f39bfc2c14684c063a0\n0032want 0992a6293c77c3ef1f9a8d37b774deddcbef2e23\n0032want f7ffa60396d99f61315a5a9a225fc32eb6228fdb\n0032want bbe5223a9a4f84e7c3d7206f4bdd062239e8309b\n0032want 15e1fb0f45ffdb9d4d07ccf23b8971fca09ab2ca\n0032want 15e1fb0f45ffdb9d4d07ccf23b8971fca09ab2ca\n0009done\n0000"}
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 519"], "body": "MDBiZDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgY2M0ZDVlMDVlMWIzMTg0ZjQ3MTZlZDE1OGQyNWI4Y2ZhZTM4NzU0MyByZWZzL2hlYWRzL2RvY2tlci1idWlsZAAgcmVwb3J0LXN0YXR1cy12MiBzaWRlLWJhbmQtNjRrIHF1aWV0IG9iamVjdC1mb3JtYXQ9c2hhMSBhZ2VudD1naXQvMi40OS4xLUxpbnV4MDAwMFBBQ0sAAAACAAAABJENeJyVi10KwjAQBt9zirwLkp+2mwURffcSm80XLLS2tBE8vvUIzsvAwLQNsIQsA+D7mAjJkfOSMVTppEKZNUCVAwezyoZXs445yBA4KpFGVF9ZUomUibqCUjSjBoRo5N2ey2bv06iwF/npho/M64SzLvPVeuIupuD73p7cgTnqPLaGvybzGPdm6zhhN18fLT8K8AKeTXt+RMTzqiOmBNVYae5fWGAYnHice8b0jGmDCaOIb2jBcr50ZZ79x/sfSmXMOBZVkFs42YNRDgDQmAyf9gKBbcdPQPqMH4Wr8ShABeE9G0Q2EXic62RcwzjBRCYns7hErziDQav39///EVurZi0SCOP5u3X/31Yu/om6MQA3nhDoN3icyylW0M1J5AIACRECBP3ERXDJ4M4Guqo/ktYk+wdQ6W4g"}
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 193"], "body": "00bdcc4d5e05e1b3184f4716ed158d25b8cfae387543 0000000000000000000000000000000000000000 refs/heads/docker-build\u0000 report-status-v2 side-band-64k quiet object-format=sha1 agent=git/2.49.1-Linux0000"}
{"method": "GET", "target": "/alice/powershell-scripts.git/info/refs?service=git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache", "Git-Protocol: version=2"], "body": ""}
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/explore/repos", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/explore/users?q=alice", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/explore/users?q=alice", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/alice/tools/raw/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/alice/tools/releases/tag/v1.1.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools/1.2.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/container/python3-app/1.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/user/login?redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/alice/tools/releases/download/v1.1.0/tools-linux-amd64.tar.gz", "headers": ["user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "accept: */*", "accept-encoding: gzip,deflate,br", "cookie: redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Host: example.com", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/alice/tools/releases/download/v1.2.0/install-script.sh", "headers": ["user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "accept: */*", "accept-encoding: gzip,deflate,br", "cookie: redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Host: example.com", "Connection: keep-alive"], "body": ""}
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 43", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login?redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml"], "body": "user_name=alice&password=Hv4-mQz8-Lr2x-Tp9w"}
{"method": "GET", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login?redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/eventsource.sharedworker.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/install-script/1.0.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages?q=python3&type=container", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/-/packages?q=python3&type=container", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/container/python3-app/1.0/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/_delete/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/context-popup.21e2b8da.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/_delete/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 168", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "last_commit=0992a6293c77c3ef1f9a8d37b774deddcbef2e23&commit_summary=Remove+.htaccess&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"method": "GET", "target": "/alice/tools/src/branch/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DFile%2B%2522.htaccess%2522%2Bhas%2Bbeen%2Bdeleted."], "body": ""}
{"method": "GET", "target": "/assets/css/repo-branch-tag-selector.d9ca1da2.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/src/branch/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/repo-branch-tag-selector.f396185d.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/src/branch/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 186", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "last_commit=cf1f6e0dcb6e2685ce4ffcc1225d31c65cd64d26&commit_summary=Remove+scripts%2Finstall-script.sh&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"method": "GET", "target": "/alice/tools/src/branch/main/scripts", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DFile%2B%2522scripts%252Finstall-script.sh%2522%2Bhas%2Bbeen%2Bdeleted."], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/branches/_new/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 28", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/src/branch/main", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "new_branch_name=docker-build"}
{"method": "GET", "target": "/alice/tools/src/branch/docker-build/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/src/branch/main", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DBranch%2B%2522docker-build%2522%2Bhas%2Bbeen%2Bcreated."], "body": ""}
{"method": "POST", "target": "/alice/tools/branches/delete?name=docker-build&page=1", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 44", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylgvDcSqmeTmS8iNm", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "------WebKitFormBoundarylgvDcSqmeTmS8iNm--\r\n"}
{"method": "GET", "target": "/alice/tools/branches?page=1", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DBranch%2B%2522docker-build%2522%2Bhas%2Bbeen%2Bdeleted."], "body": ""}
{"method": "POST", "target": "/alice/tools/settings/rename_branch", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 52", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "from=feat%2Fdocker-support&to=feat%2Fpython3-support"}
{"method": "GET", "target": "/alice/tools/branches", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DBranch%2Bfeat%252Fdocker-support%2Bwas%2Bsuccessfully%2Brenamed%2Bto%2Bfeat%252Fpython3-support."], "body": ""}
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "POST", "target": "/alice/tools/wiki/?action=_new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 180", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": "title=Python3+setup&content=Install+python3%2C+or+powershell+on+Windows.+Run+ls+-la+to+check+that+.htaccess+and+docker-compose.yml+are+there%2C+then+run+install-script.sh.&message="}
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=ls%20-la", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/powershell-scripts", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/explore/repos?q=powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/css/repo-branch-tag-selector.d9ca1da2.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/powershell-scripts", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/repo-branch-tag-selector.f396185d.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/powershell-scripts", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/branches?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
{"method": "GET", "target": "/assets/js/context-popup.21e2b8da.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/branches?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/src/branch/python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""}
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""}
{"method": "GET", "target": "/acme?q=python3&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""}
+10 -12
View File
@@ -120,19 +120,17 @@ func New(topN int, instanceName string) *Metrics {
"went on without it, because GeoJS had not given it in time.", "went on without it, because GeoJS had not given it in time.",
}), }),
stateFileWrites: counterVec("sww_state_file_writes_total", stateFileWrites: counterVec("sww_state_file_writes_total",
"Writes of each state or cache file.", byFile), "Writes of each state file.", byFile),
stateFileWriteFailures: counterVec("sww_state_file_write_failures_total", stateFileWriteFailures: counterVec("sww_state_file_write_failures_total",
"Writes of each state or cache file that failed.", byFile), "Writes of each state file that failed.", byFile),
stateFileLastWrite: gaugeVec("sww_state_file_last_write_timestamp_seconds", stateFileLastWrite: gaugeVec("sww_state_file_last_write_timestamp_seconds",
"When each state or cache file was last written, in seconds since 1970.", "When each state file was last written, in seconds since 1970.", byFile),
byFile),
stateFileSize: gaugeVec("sww_state_file_size_bytes", stateFileSize: gaugeVec("sww_state_file_size_bytes",
"The size of each state or cache file, as it was last written.", byFile), "The size of each state file, as it was last written.", byFile),
stateFileEditsTakenIn: counterVec("sww_state_file_edits_taken_in_total", stateFileEditsTakenIn: counterVec("sww_state_file_edits_taken_in_total",
"Edits of each state or cache file taken in while running.", byFile), "Edits of each state file taken in while running.", byFile),
stateFileEditsSetAside: counterVec("sww_state_file_edits_set_aside_total", stateFileEditsSetAside: counterVec("sww_state_file_edits_set_aside_total",
"Edits of each state or cache file renamed to <name>.bad because they "+ "Edits of each state file renamed to <name>.bad because they did not parse.",
"did not parse.",
byFile), byFile),
} }
@@ -449,8 +447,8 @@ func (m *Metrics) CoreRuleSetMatched(mode string, id int) {
m.coreRuleSetMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc() m.coreRuleSetMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
} }
// StateFileWritten counts a write of the state or cache file name, of size // StateFileWritten counts a write of the state file name, of size bytes,
// bytes, that ended with err. // that ended with err.
func (m *Metrics) StateFileWritten(name string, size int, err error) { func (m *Metrics) StateFileWritten(name string, size int, err error) {
m.stateFileWrites.WithLabelValues(name).Inc() m.stateFileWrites.WithLabelValues(name).Inc()
@@ -468,13 +466,13 @@ func (m *Metrics) StateFileWritten(name string, size int, err error) {
m.stateFileSize.WithLabelValues(name).Set(float64(size)) m.stateFileSize.WithLabelValues(name).Set(float64(size))
} }
// StateFileEditTakenIn counts an admin's edit of the state or cache file name // StateFileEditTakenIn counts an admin's edit of the state file name
// taken in while sww runs. // taken in while sww runs.
func (m *Metrics) StateFileEditTakenIn(name string) { func (m *Metrics) StateFileEditTakenIn(name string) {
m.stateFileEditsTakenIn.WithLabelValues(name).Inc() m.stateFileEditsTakenIn.WithLabelValues(name).Inc()
} }
// StateFileEditSetAside counts an admin's edit of the state or cache file name // StateFileEditSetAside counts an admin's edit of the state file name
// renamed to name.bad because it did not parse. // renamed to name.bad because it did not parse.
func (m *Metrics) StateFileEditSetAside(name string) { func (m *Metrics) StateFileEditSetAside(name string) {
m.stateFileEditsSetAside.WithLabelValues(name).Inc() m.stateFileEditsSetAside.WithLabelValues(name).Inc()
+24 -40
View File
@@ -36,21 +36,22 @@ var (
"checks spent; none is made until the day ends at 00:00 UTC") "checks spent; none is made until the day ends at 00:00 UTC")
) )
// Score is what AbuseIPDB said about a client, as abuseipdb-scores.json // Score is what AbuseIPDB said about a client, as reputation.json holds
// holds it: the client, an IPv4 address or an IPv6 group, its abuse // it: the client, an IPv4 address or an IPv6 group, its abuse confidence
// confidence score, from 0 to 100, and when AbuseIPDB answered. // score, from 0 to 100, and when AbuseIPDB answered.
type Score struct { type Score struct {
Client netip.Prefix `json:"client"` Client netip.Prefix `json:"client"`
Score int64 `json:"score"` Score int64 `json:"score"`
Fetched time.Time `json:"fetched"` Fetched time.Time `json:"fetched"`
} }
// ChecksSpent is what abuseipdb-checks-spent.json keeps of the checks of // Checks are what reputation.json keeps of the checks of clients with
// clients with AbuseIPDB: the day, in UTC, of the checks Spent counts, // AbuseIPDB: the day, in UTC, of the checks Spent counts, zero before the
// zero before the first. // first, and the scores still in use.
type ChecksSpent struct { type Checks struct {
Day civil.Date Day civil.Date `json:"day,omitzero"`
Spent int Spent int `json:"spent"`
Scores []Score `json:"scores"`
} }
// AbuseIPDBParams are what NewAbuseIPDB needs. // AbuseIPDBParams are what NewAbuseIPDB needs.
@@ -195,35 +196,35 @@ func (a *AbuseIPDB) BudgetLeft() int {
return max(a.params.DailyBudget-a.spent, 0) return max(a.params.DailyBudget-a.spent, 0)
} }
// Snapshot returns every score still in use, sorted by client, as // Snapshot returns the checks spent and every score still in use, sorted
// abuseipdb-scores.json keeps them. // by client, as reputation.json keeps them.
func (a *AbuseIPDB) Snapshot() []Score { func (a *AbuseIPDB) Snapshot() Checks {
a.mu.Lock() a.mu.Lock()
now := a.params.Now() now := a.params.Now()
scores := make([]Score, 0, a.scores.Len()) checks := Checks{Day: a.day, Spent: a.spent, Scores: make([]Score, 0, a.scores.Len())}
for _, kept := range a.scores.Values() { for _, kept := range a.scores.Values() {
if now.Sub(kept.Fetched) < a.params.CacheTTL { if now.Sub(kept.Fetched) < a.params.CacheTTL {
scores = append(scores, kept) checks.Scores = append(checks.Scores, kept)
} }
} }
a.mu.Unlock() a.mu.Unlock()
slices.SortFunc(scores, func(x, y Score) int { slices.SortFunc(checks.Scores, func(x, y Score) int {
return x.Client.Compare(y.Client) return x.Client.Compare(y.Client)
}) })
return scores return checks
} }
// Load keeps scores, read from abuseipdb-scores.json, in place of those // Load keeps checks, read from reputation.json, in place of those it
// it keeps, but for the scores past maxVerdicts, those fetched longest // keeps, but for the scores past maxVerdicts, those fetched longest ago.
// ago. One fetched CacheTTL ago or more is neither used nor written, as // One fetched CacheTTL ago or more is neither used nor written, as for any
// for any score. // score.
func (a *AbuseIPDB) Load(scores []Score) { func (a *AbuseIPDB) Load(checks Checks) {
scores = slices.Clone(scores) scores := slices.Clone(checks.Scores)
slices.SortStableFunc(scores, func(x, y Score) int { slices.SortStableFunc(scores, func(x, y Score) int {
return x.Fetched.Compare(y.Fetched) return x.Fetched.Compare(y.Fetched)
}) })
@@ -231,6 +232,7 @@ func (a *AbuseIPDB) Load(scores []Score) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
a.day, a.spent = checks.Day, checks.Spent
a.scores.Purge() a.scores.Purge()
for _, kept := range scores { for _, kept := range scores {
@@ -238,24 +240,6 @@ func (a *AbuseIPDB) Load(scores []Score) {
} }
} }
// ChecksSpent returns the checks spent, as abuseipdb-checks-spent.json
// keeps them.
func (a *AbuseIPDB) ChecksSpent() ChecksSpent {
a.mu.Lock()
defer a.mu.Unlock()
return ChecksSpent{Day: a.day, Spent: a.spent}
}
// LoadChecksSpent keeps checks, read from abuseipdb-checks-spent.json, in
// place of the checks spent.
func (a *AbuseIPDB) LoadChecksSpent(checks ChecksSpent) {
a.mu.Lock()
defer a.mu.Unlock()
a.day, a.spent = checks.Day, checks.Spent
}
// check checks client with AbuseIPDB by addr, one of its addresses, keeps // check checks client with AbuseIPDB by addr, one of its addresses, keeps
// the score as client's, and notes the check as no longer under way. A // the score as client's, and notes the check as no longer under way. A
// check that fails gives no score: it is counted, logged and raised as a // check that fails gives no score: it is counted, logged and raised as a
+16 -15
View File
@@ -17,7 +17,6 @@ import (
"testing/synctest" "testing/synctest"
"time" "time"
"cloud.google.com/go/civil"
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/metrics" "sneak.berlin/go/smallwebwaf/internal/metrics"
"sneak.berlin/go/smallwebwaf/internal/reputation" "sneak.berlin/go/smallwebwaf/internal/reputation"
@@ -180,13 +179,11 @@ func TestDailyBudgetKeptAcrossARestartAndWholeAgainAsTheDayEnds(t *testing.T) {
t.Errorf("logged\n%s\nwant the budget used up", log.String()) t.Errorf("logged\n%s\nwant the budget used up", log.String())
} }
// Restarted with what abuseipdb-scores.json and // Restarted with what reputation.json keeps, it uses the scores, and
// abuseipdb-checks-spent.json keep, it uses the scores, and checks // checks no client until the day ends.
// no client until the day ends.
restarted := &abuseIPDBStandIn{} restarted := &abuseIPDBStandIn{}
again := newAbuseIPDB(restarted, p) again := newAbuseIPDB(restarted, p)
again.Load(checker.Snapshot()) again.Load(checker.Snapshot())
again.LoadChecksSpent(checker.ChecksSpent())
wantScore(t, again, suspect, true, 100, true) wantScore(t, again, suspect, true, 100, true)
wantBudgetLeft(t, again, 0) wantBudgetLeft(t, again, 0)
@@ -227,9 +224,13 @@ func TestChecksAreCountedInUTCDaysWhateverTheClocksZone(t *testing.T) {
time.Sleep(2 * time.Hour) time.Sleep(2 * time.Hour)
wantBudgetLeft(t, checker, 0) wantBudgetLeft(t, checker, 0)
want := civil.Date{Year: 2000, Month: 1, Day: 1} encoded, err := json.Marshal(checker.Snapshot())
if day := checker.ChecksSpent().Day; day != want { if err != nil {
t.Errorf("checks spent on %s, want on %s", day, want) t.Fatalf("encode: %v", err)
}
if !strings.Contains(string(encoded), `"day":"2000-01-01"`) {
t.Errorf("reputation.json would hold %s, want the day 2000-01-01", encoded)
} }
// At 00:00 UTC the budget is whole again. // At 00:00 UTC the budget is whole again.
@@ -301,7 +302,7 @@ func TestFailedCheckGivesNoScoreAndNoClientIsCheckedForAMinute(t *testing.T) {
wantChecked(t, abuseIPDB, suspect, other) wantChecked(t, abuseIPDB, suspect, other)
wantFailures(t, checker, 2) wantFailures(t, checker, 2)
if scores := checker.Snapshot(); len(scores) != 0 { if scores := checker.Snapshot().Scores; len(scores) != 0 {
t.Errorf("scores %+v, want none", scores) t.Errorf("scores %+v, want none", scores)
} }
@@ -398,7 +399,7 @@ func TestKeyIsSentInTheKeyHeaderAndNeverShown(t *testing.T) {
for name, shown := range map[string]string{ for name, shown := range map[string]string{
"the log": log.String(), "the alerts": string(alerted), "the log": log.String(), "the alerts": string(alerted),
"the metrics": scrapeMetrics(t, m), "abuseipdb-scores.json": string(kept), "the metrics": scrapeMetrics(t, m), "reputation.json": string(kept),
} { } {
if strings.Contains(shown, key) { if strings.Contains(shown, key) {
t.Errorf("%s shows the key:\n%s", name, shown) t.Errorf("%s shows the key:\n%s", name, shown)
@@ -461,12 +462,12 @@ func TestScoreFetchedATTLAgoIsNeitherUsedNorKept(t *testing.T) {
Fetched: now.Add(-cacheTTL), Fetched: now.Add(-cacheTTL),
} }
checker.Load([]reputation.Score{stale, inUse}) checker.Load(reputation.Checks{Scores: []reputation.Score{stale, inUse}})
wantScore(t, checker, suspect, false, 100, true) wantScore(t, checker, suspect, false, 100, true)
wantScore(t, checker, other, false, 0, false) wantScore(t, checker, other, false, 0, false)
got := checker.Snapshot() got := checker.Snapshot().Scores
if !reflect.DeepEqual(got, []reputation.Score{inUse}) { if !reflect.DeepEqual(got, []reputation.Score{inUse}) {
t.Errorf("scores %+v, want only %+v", got, inUse) t.Errorf("scores %+v, want only %+v", got, inUse)
} }
@@ -480,7 +481,7 @@ func TestAtMost100000ScoresKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.T)
p.Now = func() time.Time { return now } p.Now = func() time.Time { return now }
checker := reputation.NewAbuseIPDB(p) checker := reputation.NewAbuseIPDB(p)
// 100,001 scores, listed by client, as abuseipdb-scores.json lists them, each // 100,001 scores, listed by client, as reputation.json lists them, each
// fetched a millisecond before the one before it: the last is one too // fetched a millisecond before the one before it: the last is one too
// many. // many.
const count = 100001 const count = 100001
@@ -496,9 +497,9 @@ func TestAtMost100000ScoresKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.T)
addr = addr.Next() addr = addr.Next()
} }
checker.Load(scores) checker.Load(reputation.Checks{Scores: scores})
got := checker.Snapshot() got := checker.Snapshot().Scores
if len(got) != count-1 || !slices.Contains(got, scores[0]) || if len(got) != count-1 || !slices.Contains(got, scores[0]) ||
slices.Contains(got, scores[count-1]) { slices.Contains(got, scores[count-1]) {
t.Errorf("%d scores kept, want all but the one fetched longest ago", len(got)) t.Errorf("%d scores kept, want all but the one fetched longest ago", len(got))
+2 -2
View File
@@ -291,7 +291,7 @@ func crowdSecFailures() []crowdSecFailure {
// other than its decision list, such as the one a redirect names, and that // other than its decision list, such as the one a redirect names, and that
// the key is in none of what the fetches leave behind: log, the process // the key is in none of what the fetches leave behind: log, the process
// log, the alerts waiting in queue, and the copies of lists, which // log, the alerts waiting in queue, and the copies of lists, which
// lists.json keeps. // reputation.json keeps.
func wantKeyNotShown( func wantKeyNotShown(
t *testing.T, e *engine, log string, lists *reputation.Lists, queue *alerts.Queue, t *testing.T, e *engine, log string, lists *reputation.Lists, queue *alerts.Queue,
) { ) {
@@ -326,7 +326,7 @@ func TestCrowdSecDecisionListKeptAcrossARestartEndsWhenItsDecisionsDo(t *testing
lists := start(t, e, crowdSecParams()) lists := start(t, e, crowdSecParams())
kept := lists.Snapshot() kept := lists.Snapshot()
// Restarted half an hour later with what lists.json keeps, and // Restarted half an hour later with what reputation.json keeps, and
// the engine down, the decision still bans, until the end it had at // the engine down, the decision still bans, until the end it had at
// the fetch, half an hour on. // the fetch, half an hour on.
time.Sleep(30 * time.Minute) time.Sleep(30 * time.Minute)
+3 -3
View File
@@ -40,7 +40,7 @@ var (
errNotListing = errors.New("the answer is outside 127.0.0.0/8") errNotListing = errors.New("the answer is outside 127.0.0.0/8")
) )
// Verdict is what a zone said about a client, as dnsbl.json holds // Verdict is what a zone said about a client, as reputation.json holds
// it: the zone, the client's address, whether the zone lists it, and when // it: the zone, the client's address, whether the zone lists it, and when
// the zone answered. // the zone answered.
type Verdict struct { type Verdict struct {
@@ -187,7 +187,7 @@ func (d *DNSBL) Failures(zone string) int {
} }
// Snapshot returns every verdict still in use, sorted by client, then by // Snapshot returns every verdict still in use, sorted by client, then by
// zone, as dnsbl.json lists them. // zone, as reputation.json lists them.
func (d *DNSBL) Snapshot() []Verdict { func (d *DNSBL) Snapshot() []Verdict {
d.mu.Lock() d.mu.Lock()
@@ -209,7 +209,7 @@ func (d *DNSBL) Snapshot() []Verdict {
return verdicts return verdicts
} }
// Load keeps verdicts, read from dnsbl.json, in place of those it // Load keeps verdicts, read from reputation.json, in place of those it
// keeps, but for those of a zone SWW_DNSBL_ZONES does not name, and, // keeps, but for those of a zone SWW_DNSBL_ZONES does not name, and,
// past maxVerdicts, those fetched longest ago. One fetched CacheTTL ago or // past maxVerdicts, those fetched longest ago. One fetched CacheTTL ago or
// more is neither used nor written, as for any verdict. // more is neither used nor written, as for any verdict.
+2 -2
View File
@@ -389,7 +389,7 @@ func TestVerdictsKeptAcrossARestart(t *testing.T) {
t.Errorf("verdicts %+v, want %+v", kept, want) t.Errorf("verdicts %+v, want %+v", kept, want)
} }
// Restarted an hour later with what dnsbl.json keeps, it uses // Restarted an hour later with what reputation.json keeps, it uses
// the verdicts, and asks the zone nothing, until the TTL has passed // the verdicts, and asks the zone nothing, until the TTL has passed
// since they were fetched. // since they were fetched.
time.Sleep(time.Hour) time.Sleep(time.Hour)
@@ -446,7 +446,7 @@ func TestAtMost100000VerdictsKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.
p.Now = func() time.Time { return now } p.Now = func() time.Time { return now }
dnsbl := reputation.NewDNSBL(p) dnsbl := reputation.NewDNSBL(p)
// 100,001 verdicts, listed by client, as dnsbl.json lists them, // 100,001 verdicts, listed by client, as reputation.json lists them,
// each fetched a millisecond before the one before it: the last is one // each fetched a millisecond before the one before it: the last is one
// too many. // too many.
const count = 100001 const count = 100001
+7 -7
View File
@@ -7,8 +7,8 @@
// and when each was last tried. It also asks the DNSBL zones of // and when each was last tried. It also asks the DNSBL zones of
// SWW_DNSBL_ZONES about clients, and keeps their verdicts, and checks // SWW_DNSBL_ZONES about clients, and keeps their verdicts, and checks
// clients with AbuseIPDB, and keeps their scores and the checks spent // clients with AbuseIPDB, and keeps their scores and the checks spent
// today. The state package writes all of these to files and reads them // today. The state package writes all of these to reputation.json and
// from there, so that a restart keeps them too. // reads them from it, so that a restart keeps them too.
package reputation package reputation
import ( import (
@@ -54,7 +54,7 @@ var (
"does not give an address or a netblock and a duration, such as 4h0m0s") "does not give an address or a netblock and a duration, such as 4h0m0s")
) )
// List is a list as lists.json holds it: the URL it is fetched from, // List is a list as reputation.json holds it: the URL it is fetched from,
// when it was last tried, the fetch failed or not, and its last good copy: // when it was last tried, the fetch failed or not, and its last good copy:
// when that was fetched, and its lines, as fetched, comment lines // when that was fetched, and its lines, as fetched, comment lines
// included, both left out while no fetch of it has succeeded. // included, both left out while no fetch of it has succeeded.
@@ -106,7 +106,7 @@ type Lists struct {
lists map[string]*list lists map[string]*list
} }
// list is one list: what lists.json keeps of it, its last try, zero // list is one list: what reputation.json keeps of it, its last try, zero
// before the first, and its last good copy, what that copy says, and how // before the first, and its last good copy, what that copy says, and how
// many fetches of it failed. // many fetches of it failed.
type list struct { type list struct {
@@ -272,7 +272,7 @@ func (l *Lists) Failures(listURL string) int {
// Run fetches each list once it is due, as due tells, until ctx is done. A // Run fetches each list once it is due, as due tells, until ctx is done. A
// list never tried is fetched at once, and so is one that is due by its // list never tried is fetched at once, and so is one that is due by its
// last try or copy read from lists.json. // last try or copy read from reputation.json.
func (l *Lists) Run(ctx context.Context) { func (l *Lists) Run(ctx context.Context) {
if len(l.lists) == 0 { if len(l.lists) == 0 {
return return
@@ -292,7 +292,7 @@ func (l *Lists) Run(ctx context.Context) {
} }
// Snapshot returns each list that has been tried, with its copy, if it // Snapshot returns each list that has been tried, with its copy, if it
// has one, sorted by URL, as lists.json lists them. // has one, sorted by URL, as reputation.json lists them.
func (l *Lists) Snapshot() []List { func (l *Lists) Snapshot() []List {
l.mu.Lock() l.mu.Lock()
@@ -313,7 +313,7 @@ func (l *Lists) Snapshot() []List {
return tried return tried
} }
// Load puts lists, read from lists.json, in place of the last tries // Load puts lists, read from reputation.json, in place of the last tries
// and copies held. A list Params does not name is dropped. A copy with a // and copies held. A list Params does not name is dropped. A copy with a
// line that parse refuses is an error, and then nothing changes. // line that parse refuses is an error, and then nothing changes.
func (l *Lists) Load(lists []List) error { func (l *Lists) Load(lists []List) error {
+3 -3
View File
@@ -260,7 +260,7 @@ func TestKeptCopyIsFetchedAgainOnceRefreshHasPassedSinceItWasFetched(t *testing.
lists.SetTransport(servers) lists.SetTransport(servers)
// drop.txt was fetched an hour ago, and tor.txt a refresh ago, as // drop.txt was fetched an hour ago, and tor.txt a refresh ago, as
// lists.json says at start. // reputation.json says at start.
err := lists.Load([]reputation.List{ err := lists.Load([]reputation.List{
{URL: dropURL, Fetched: time.Now().Add(-time.Hour), Lines: []string{"203.0.113.9"}}, {URL: dropURL, Fetched: time.Now().Add(-time.Hour), Lines: []string{"203.0.113.9"}},
{URL: torURL, Fetched: time.Now().Add(-refresh), Lines: []string{"198.51.100.9"}}, {URL: torURL, Fetched: time.Now().Add(-refresh), Lines: []string{"198.51.100.9"}},
@@ -297,7 +297,7 @@ func TestRestartWaitsRefreshAfterTheLastTryEvenOneThatFailed(t *testing.T) {
time.Sleep(refresh) time.Sleep(refresh)
wantFetches(t, servers, 2) wantFetches(t, servers, 2)
// Restarted with what lists.json keeps, it waits a refresh // Restarted with what reputation.json keeps, it waits a refresh
// after the failed try, as it does while it runs. // after the failed try, as it does while it runs.
restarted := &standIn{lists: map[string]string{dropURL: "198.51.100.2\n"}} restarted := &standIn{lists: map[string]string{dropURL: "198.51.100.2\n"}}
again := reputation.New(params(dropURL)) again := reputation.New(params(dropURL))
@@ -353,7 +353,7 @@ func TestFetchCutOffAsItStopsIsNoFailureButARestartWaitsForIt(t *testing.T) {
waiting(queue)) waiting(queue))
} }
// Restarted an hour later with what lists.json keeps, it fetches // Restarted an hour later with what reputation.json keeps, it fetches
// tor.txt, never tried, at once, and drop.txt a refresh after its // tor.txt, never tried, at once, and drop.txt a refresh after its
// cut-off try. // cut-off try.
time.Sleep(time.Hour) time.Sleep(time.Hour)
+27 -86
View File
@@ -1,8 +1,6 @@
// Package routewatch asks a routewatch instance about client addresses, in the // Package routewatch asks a routewatch instance about client addresses, in the
// background, and keeps its answers. A routewatch instance knows, for an // background, and keeps its answers in memory. A routewatch instance knows, for
// address, the netblock it is announced in and the AS that originates it. The // an address, the netblock it is announced in and the AS that originates it.
// answers are kept in memory, for at most 100,000 clients and for an hour each,
// and are written to routewatch.json and read from it by the state package.
package routewatch package routewatch
import ( import (
@@ -15,7 +13,6 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"slices"
"sync" "sync"
"time" "time"
@@ -68,17 +65,6 @@ type Route struct {
ASN uint32 ASN uint32
} }
// Answer is what routewatch answered about a client, as routewatch.json holds
// it: the netblock the client's address is announced in, none for an address
// without a live route, the AS that originates it, and when routewatch
// answered.
type Answer struct {
Client netip.Prefix `json:"client"`
Netblock netip.Prefix `json:"netblock"`
ASN uint32 `json:"asn"`
Answered time.Time `json:"answered"`
}
// Params are what New needs. // Params are what New needs.
type Params struct { type Params struct {
// URL is where routewatch is (SWW_ROUTEWATCH_URL). // URL is where routewatch is (SWW_ROUTEWATCH_URL).
@@ -98,16 +84,23 @@ type Client struct {
mu sync.Mutex mu sync.Mutex
// answers are by client. Each is added as it comes and never moved up, // answers are by client. Each is added as it comes and never moved up,
// so that the one that came longest ago is the first dropped. // so that the one that came longest ago is the first dropped.
answers *simplelru.LRU[netip.Prefix, Answer] answers *simplelru.LRU[netip.Prefix, answer]
// asking are the clients whose query is under way. // asking are the clients whose query is under way.
asking map[netip.Prefix]bool asking map[netip.Prefix]bool
// retryAt is when routewatch may be asked again after a query failed. // retryAt is when routewatch may be asked again after a query failed.
retryAt time.Time retryAt time.Time
} }
// answer is one kept answer: the route, or why there is none, and when it came.
type answer struct {
route Route
missing Missing
came time.Time
}
// New returns a Client with no answer yet. // New returns a Client with no answer yet.
func New(params Params) *Client { func New(params Params) *Client {
answers, err := simplelru.NewLRU[netip.Prefix, Answer](maxAnswers, nil) answers, err := simplelru.NewLRU[netip.Prefix, answer](maxAnswers, nil)
if err != nil { if err != nil {
panic(err) // NewLRU fails only for a size below one panic(err) // NewLRU fails only for a size below one
} }
@@ -135,12 +128,8 @@ func (c *Client) Route(
now := c.params.Now() now := c.params.Now()
kept, found := c.answers.Peek(client) kept, found := c.answers.Peek(client)
if found && now.Sub(kept.Answered) < answerKept { if found && now.Sub(kept.came) < answerKept {
if !kept.Netblock.IsValid() { return kept.route, kept.missing
return Route{}, NoRoute
}
return Route{Netblock: kept.Netblock, ASN: kept.ASN}, ""
} }
if now.Before(c.retryAt) { if now.Before(c.retryAt) {
@@ -156,58 +145,12 @@ func (c *Client) Route(
return Route{}, NoAnswerYet return Route{}, NoAnswerYet
} }
// Snapshot returns every answer still in use, sorted by client, as
// routewatch.json lists them.
func (c *Client) Snapshot() []Answer {
c.mu.Lock()
now := c.params.Now()
answers := make([]Answer, 0, c.answers.Len())
for _, kept := range c.answers.Values() {
if now.Sub(kept.Answered) < answerKept {
answers = append(answers, kept)
}
}
c.mu.Unlock()
slices.SortFunc(answers, func(a, b Answer) int {
return a.Client.Compare(b.Client)
})
return answers
}
// Load keeps answers, read from routewatch.json, in place of those it keeps,
// but for those that came answerKept ago or more and, past maxAnswers, those
// that came longest ago.
func (c *Client) Load(answers []Answer) {
answers = slices.Clone(answers)
slices.SortStableFunc(answers, func(a, b Answer) int {
return a.Answered.Compare(b.Answered)
})
c.mu.Lock()
defer c.mu.Unlock()
c.answers.Purge()
now := c.params.Now()
for _, kept := range answers {
if now.Sub(kept.Answered) < answerKept {
c.answers.Add(kept.Client, kept)
}
}
}
// ask asks routewatch about addr, keeps the answer as client's, and notes the // ask asks routewatch about addr, keeps the answer as client's, and notes the
// query as no longer under way. A query that fails gives no answer, and // query as no longer under way. A query that fails gives no answer, and
// routewatch is not asked again for failureKept. Only the first failure of // routewatch is not asked again for failureKept. Only the first failure of
// those under way is logged. // those under way is logged.
func (c *Client) ask(ctx context.Context, client netip.Prefix, addr netip.Addr) { func (c *Client) ask(ctx context.Context, client netip.Prefix, addr netip.Addr) {
route, err := c.query(ctx, addr) route, missing, err := c.query(ctx, addr)
now := c.params.Now() now := c.params.Now()
c.mu.Lock() c.mu.Lock()
@@ -217,9 +160,7 @@ func (c *Client) ask(ctx context.Context, client netip.Prefix, addr netip.Addr)
firstFailure := err != nil && !now.Before(c.retryAt) firstFailure := err != nil && !now.Before(c.retryAt)
if err == nil { if err == nil {
c.answers.Add(client, Answer{ c.answers.Add(client, answer{route: route, missing: missing, came: now})
Client: client, Netblock: route.Netblock, ASN: route.ASN, Answered: now,
})
} else { } else {
c.retryAt = now.Add(failureKept) c.retryAt = now.Add(failureKept)
} }
@@ -233,19 +174,19 @@ func (c *Client) ask(ctx context.Context, client netip.Prefix, addr netip.Addr)
// query asks routewatch's IP info API about addr. An answer other than 200, one // query asks routewatch's IP info API about addr. An answer other than 200, one
// that cannot be read, and none within queryTimeout, fail. A result with no // that cannot be read, and none within queryTimeout, fail. A result with no
// netblock is an address without a live route, and gives a Route without one. // netblock is an address without a live route.
func (c *Client) query(ctx context.Context, addr netip.Addr) (Route, error) { func (c *Client) query(ctx context.Context, addr netip.Addr) (Route, Missing, error) {
ctx, cancel := context.WithTimeout(ctx, queryTimeout) ctx, cancel := context.WithTimeout(ctx, queryTimeout)
defer cancel() defer cancel()
endpoint, err := url.JoinPath(c.params.URL, "api/v1/ip", addr.String()) endpoint, err := url.JoinPath(c.params.URL, "api/v1/ip", addr.String())
if err != nil { if err != nil {
return Route{}, fmt.Errorf("make the URL: %w", err) return Route{}, "", fmt.Errorf("make the URL: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
if err != nil { if err != nil {
return Route{}, fmt.Errorf("make the request: %w", err) return Route{}, "", fmt.Errorf("make the request: %w", err)
} }
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
@@ -254,7 +195,7 @@ func (c *Client) query(ctx context.Context, addr netip.Addr) (Route, error) {
if err != nil { if err != nil {
// Do's error names the URL, which holds the client's address, which // Do's error names the URL, which holds the client's address, which
// is not to be logged: only what went wrong is kept. // is not to be logged: only what went wrong is kept.
return Route{}, fmt.Errorf("ask routewatch: %w", errors.Unwrap(err)) return Route{}, "", fmt.Errorf("ask routewatch: %w", errors.Unwrap(err))
} }
defer func() { defer func() {
@@ -262,14 +203,14 @@ func (c *Client) query(ctx context.Context, addr netip.Addr) (Route, error) {
}() }()
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return Route{}, fmt.Errorf("%w %s", errStatus, res.Status) return Route{}, "", fmt.Errorf("%w %s", errStatus, res.Status)
} }
return readAnswer(io.LimitReader(res.Body, maxAnswerBytes)) return readAnswer(io.LimitReader(res.Body, maxAnswerBytes))
} }
// readAnswer reads routewatch's answer about one address. // readAnswer reads routewatch's answer about one address.
func readAnswer(body io.Reader) (Route, error) { func readAnswer(body io.Reader) (Route, Missing, error) {
var decoded struct { var decoded struct {
Data struct { Data struct {
Results []struct { Results []struct {
@@ -281,22 +222,22 @@ func readAnswer(body io.Reader) (Route, error) {
err := json.NewDecoder(body).Decode(&decoded) err := json.NewDecoder(body).Decode(&decoded)
if err != nil { if err != nil {
return Route{}, fmt.Errorf("read the answer: %w", err) return Route{}, "", fmt.Errorf("read the answer: %w", err)
} }
if len(decoded.Data.Results) == 0 { if len(decoded.Data.Results) == 0 {
return Route{}, errNoResult return Route{}, "", errNoResult
} }
result := decoded.Data.Results[0] result := decoded.Data.Results[0]
if result.Netblock == "" { if result.Netblock == "" {
return Route{}, nil return Route{}, NoRoute, nil
} }
netblock, err := netip.ParsePrefix(result.Netblock) netblock, err := netip.ParsePrefix(result.Netblock)
if err != nil { if err != nil {
return Route{}, fmt.Errorf("%w: %q", errNotNetblock, result.Netblock) return Route{}, "", fmt.Errorf("%w: %q", errNotNetblock, result.Netblock)
} }
return Route{Netblock: netblock.Masked(), ASN: result.ASN}, nil return Route{Netblock: netblock.Masked(), ASN: result.ASN}, "", nil
} }
+13 -139
View File
@@ -7,12 +7,10 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"slices"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
"testing/synctest"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/routewatch" "sneak.berlin/go/smallwebwaf/internal/routewatch"
@@ -115,28 +113,6 @@ func answering(body string) http.HandlerFunc {
} }
} }
// silentURL is where a routewatch that never answers is: a query to it waits,
// without the network, until it is abandoned. TestMain registers it with Go's
// default transport, through which the Client asks.
const silentURL = "silent://routewatch/"
func TestMain(m *testing.M) {
transport, _ := http.DefaultTransport.(*http.Transport)
transport.RegisterProtocol("silent", silentRoutewatch{})
m.Run()
}
// silentRoutewatch is the routewatch at silentURL.
type silentRoutewatch struct{}
// RoundTrip waits until req is abandoned.
func (silentRoutewatch) RoundTrip(req *http.Request) (*http.Response, error) {
<-req.Context().Done()
return nil, req.Context().Err()
}
func (f *fixture) route() (routewatch.Route, routewatch.Missing) { func (f *fixture) route() (routewatch.Route, routewatch.Missing) {
addr := netip.MustParseAddr(clientAddr) addr := netip.MustParseAddr(clientAddr)
client := netip.PrefixFrom(addr, addr.BitLen()) client := netip.PrefixFrom(addr, addr.BitLen())
@@ -311,29 +287,20 @@ func TestFailureIsKeptForAMinuteSoRoutewatchIsNotAskedPerRequest(t *testing.T) {
func TestHangingRoutewatchNeverDelaysAndFailsTheQuery(t *testing.T) { func TestHangingRoutewatchNeverDelaysAndFailsTheQuery(t *testing.T) {
t.Parallel() t.Parallel()
// In a synctest bubble, time passes only while every goroutine in it waits, f := newFixture(t, func(_ http.ResponseWriter, r *http.Request) {
// so Route's wait is measured exactly, however busy the host is. routewatch <-r.Context().Done()
// is the one at silentURL: one reached over the network would keep the
// bubble's clock from moving.
synctest.Test(t, func(t *testing.T) {
// Without newFixture's stand-in, which is reached over the network.
f := &fixture{t: t, log: &syncBuffer{}}
f.client = routewatch.New(routewatch.Params{
URL: silentURL, Now: time.Now,
ProcessLog: slog.New(slog.NewTextHandler(f.log, nil)),
})
began := time.Now()
_, missing := f.route()
if waited := time.Since(began); missing != routewatch.NoAnswerYet || waited != 0 {
t.Errorf("Route gave %q after %s while routewatch hangs, want %q at once",
missing, waited, routewatch.NoAnswerYet)
}
f.waitFor(routewatch.RequestFailed)
f.wantFailureLoggedWithoutTheClient()
}) })
start := time.Now()
f.route()
if elapsed := time.Since(start); elapsed > time.Second {
t.Errorf("Route took %s while routewatch hangs", elapsed)
}
f.waitFor(routewatch.RequestFailed)
f.wantFailureLoggedWithoutTheClient()
} }
func TestRepeatedRequestsMakeOneQueryAndTheAnswerExpires(t *testing.T) { func TestRepeatedRequestsMakeOneQueryAndTheAnswerExpires(t *testing.T) {
@@ -369,96 +336,3 @@ func TestRepeatedRequestsMakeOneQueryAndTheAnswerExpires(t *testing.T) {
t.Errorf("%d queries after the answer expired, want 2", got) t.Errorf("%d queries after the answer expired, want 2", got)
} }
} }
func TestSnapshotHoldsTheAnswerUntilItExpires(t *testing.T) {
t.Parallel()
f := newFixture(t, answering(routedAnswer))
f.route()
f.waitFor("")
want := []routewatch.Answer{{
Client: netip.MustParsePrefix(clientAddr + "/32"),
Netblock: netip.MustParsePrefix("192.0.2.0/24"), ASN: 64496,
Answered: f.clk.Now(),
}}
if got := f.client.Snapshot(); !slices.Equal(got, want) {
t.Errorf("snapshot %+v, want %+v", got, want)
}
f.clk.advance(time.Hour)
if got := f.client.Snapshot(); len(got) != 0 {
t.Errorf("snapshot %+v an hour after the answer, want none", got)
}
}
func TestLoadedAnswersAreUsedForAnHourFromWhenTheyCame(t *testing.T) {
t.Parallel()
f := newFixture(t, answering(routedAnswer))
now := f.clk.Now()
kept := routewatch.Answer{
Client: netip.MustParsePrefix(clientAddr + "/32"),
Netblock: netip.MustParsePrefix("198.51.100.0/24"), ASN: 64497,
Answered: now.Add(-time.Hour + time.Second),
}
stale := routewatch.Answer{
Client: netip.MustParsePrefix("192.0.2.2/32"),
Netblock: netip.MustParsePrefix("192.0.2.0/24"), ASN: 64496,
Answered: now.Add(-time.Hour),
}
f.client.Load([]routewatch.Answer{kept, stale})
if got := f.client.Snapshot(); !slices.Equal(got, []routewatch.Answer{kept}) {
t.Errorf("kept %+v, want only the answer that came less than an hour ago", got)
}
route, missing := f.route()
want := routewatch.Route{Netblock: kept.Netblock, ASN: kept.ASN}
if route != want || missing != "" {
t.Errorf("route %+v (%q), want %+v", route, missing, want)
}
if got := f.queries.Load(); got != 0 {
t.Errorf("%d queries for a client with an answer loaded, want none", got)
}
}
func TestLoadDropsTheAnswerThatCameLongestAgoFirst(t *testing.T) {
t.Parallel()
const maxAnswers = 100000
f := newFixture(t, answering(routedAnswer))
now := f.clk.Now()
// routewatch.json lists the answers by client. Here each came a
// millisecond before the one listed before it, so the last listed is
// the one that came longest ago, and the one dropped.
answers := make([]routewatch.Answer, maxAnswers+1)
addr := netip.MustParseAddr("10.0.0.0")
for i := range answers {
answers[i] = routewatch.Answer{
Client: netip.PrefixFrom(addr, addr.BitLen()),
Answered: now.Add(-time.Duration(i) * time.Millisecond),
}
addr = addr.Next()
}
f.client.Load(answers)
got := f.client.Snapshot()
if len(got) != maxAnswers {
t.Fatalf("%d answers kept, want %d", len(got), maxAnswers)
}
if got[0] != answers[0] || got[maxAnswers-1] != answers[maxAnswers-1] {
t.Errorf("answers kept from %s to %s, want from %s to %s", got[0].Client,
got[maxAnswers-1].Client, answers[0].Client, answers[maxAnswers-1].Client)
}
}
+26 -73
View File
@@ -12,6 +12,7 @@ import (
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@@ -382,20 +383,6 @@ type deadlineRecorder struct {
// stalled, if set, sends the part of the request body that never arrives. A // stalled, if set, sends the part of the request body that never arrives. A
// read deadline that has passed ends a read of it, as on a connection. // read deadline that has passed ends a read of it, as on a connection.
stalled *io.PipeWriter stalled *io.PipeWriter
// flushError, if set, is what every flush returns, and nothing is flushed.
flushError error
}
// FlushError, which http.ResponseController calls to flush, returns flushError
// if it is set, and otherwise flushes.
func (w *deadlineRecorder) FlushError() error {
if w.flushError != nil {
return w.flushError
}
w.Flush()
return nil
} }
// SetReadDeadline notes deadline. // SetReadDeadline notes deadline.
@@ -461,45 +448,6 @@ func TestABodyLeftUnreadIsReadOnlyUntilTheRequestTimeLimit(t *testing.T) {
} }
} }
func TestABodyLeftUnreadIsReadNoMoreOnceTheTimeToSendTheAnswerRunsOut(t *testing.T) {
t.Parallel()
standIn := startWAF(t, allow)
app := startApp(t, answerBeforeTheBody)
_, _, server := startServer(t, standIn.URL, app.URL, nil)
// The visitor sends the first part and a little more, and then nothing.
stalled, stop := io.Pipe()
t.Cleanup(func() { _ = stop.Close() })
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/",
io.MultiReader(bytes.NewReader(make([]byte, questionBodyMaxBytes+1)), stalled))
req.ContentLength = 2 * questionBodyMaxBytes
req.Header.Set(contentLengthHeader, strconv.Itoa(2*questionBodyMaxBytes))
// Every flush fails, as once the time to send the answer has run out. The
// default time limits are far longer than the test, so rproxy records no
// refusal.
w := &deadlineRecorder{
ResponseRecorder: httptest.NewRecorder(),
stalled: stop,
flushError: os.ErrDeadlineExceeded,
}
before := time.Now()
server.Handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status %d, want the app's 200", w.Code)
}
if w.readDeadline.Before(before) || w.readDeadline.After(time.Now()) {
t.Errorf("the reading of the body ends at %s, want now", w.readDeadline)
}
}
func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) { func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
t.Parallel() t.Parallel()
@@ -970,45 +918,49 @@ func TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOf
) { ) {
t.Parallel() t.Parallel()
// rproxy passes on a part sent at once well within this limit, even on a busy const limit = 300 * time.Millisecond
// host.
const limit = 2 * time.Second
// The app sends the parts in atOnce with its status and headers. It sends the // With two parts, rproxy fails to send the first and then waits for the
// parts in late only once rproxy has ended the request to it, which rproxy // second, which the app sends only once rproxy has ended the request to it.
// does when it cuts off the answer, so that those never reach the visitor.
for _, tc := range []struct { for _, tc := range []struct {
name string name string
atOnce, late []string parts []string
}{ }{
{"with the body in one part", nil, []string{greeting}}, {"with the body in one part", []string{greeting}},
{"with the body in two parts", []string{greeting[:5]}, []string{greeting[5:]}}, {"with the body in two parts", []string{greeting[:5], greeting[5:]}},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
headersRead := make(chan struct{})
// letAppSendBody also runs when the test ends, as the app's server waits
// for the app.
letAppSendBody := sync.OnceFunc(func() { close(headersRead) })
standIn := startWAF(t, allow) standIn := startWAF(t, allow)
// The app sends its body once the visitor has the status and headers and
// the response time limit has run out since, so that rproxy cannot send
// it. After each part, it reads the request's body, which ends when rproxy
// ends the request to it.
app := startApp(t, func(w http.ResponseWriter, r *http.Request) { app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w) rc := http.NewResponseController(w)
_ = rc.EnableFullDuplex() _ = rc.EnableFullDuplex()
w.Header().Set(contentLengthHeader, strconv.Itoa(len(greeting))) w.Header().Set(contentLengthHeader, strconv.Itoa(len(greeting)))
for _, part := range tc.atOnce {
_, _ = io.WriteString(w, part)
}
_ = rc.Flush() _ = rc.Flush()
// The request's body ends when rproxy ends the request to the app. <-headersRead
_, _ = io.Copy(io.Discard, r.Body) time.Sleep(limit)
for _, part := range tc.late { for _, part := range tc.parts {
_, _ = io.WriteString(w, part) _, _ = io.WriteString(w, part)
_ = rc.Flush() _ = rc.Flush()
_, _ = io.Copy(io.Discard, r.Body)
} }
}) })
t.Cleanup(letAppSendBody)
// The request time limit is far longer than dial's deadline. // The request time limit is far longer than dial's deadline.
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{ addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "120000", maxRequestTime: "120000",
@@ -1023,14 +975,15 @@ func TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOf
t.Fatalf("read the answer: %v", err) t.Fatalf("read the answer: %v", err)
} }
letAppSendBody()
// If the connection stays open, dial's deadline ends this read with a // If the connection stays open, dial's deadline ends this read with a
// timeout. // timeout.
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
_ = res.Body.Close() _ = res.Body.Close()
want := strings.Join(tc.atOnce, "") if len(body) != 0 || !errors.Is(err, io.ErrUnexpectedEOF) {
if string(body) != want || !errors.Is(err, io.ErrUnexpectedEOF) { t.Errorf("read %q (%v), want the connection closed", body, err)
t.Errorf("read %q (%v), want %q and the connection closed", body, err, want)
} }
}) })
} }
+15 -33
View File
@@ -272,10 +272,6 @@ func (rq *request) modifyResponse(res *http.Response) error {
rq.closeAfterAnswer() rq.closeAfterAnswer()
// An answer that arrives before the app has been sent the whole request ends
// the request, and SWRP_MAX_RESPONSE_TIME_MS starts.
rq.startResponseTimer(requestEnd)
res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq} res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq}
rq.out.afterStatus = rq.setResponseDeadline rq.out.afterStatus = rq.setResponseDeadline
@@ -390,18 +386,14 @@ func (rq *request) finish(ctx context.Context) {
// After a cut-off answer, the reading of a body that has not arrived whole // After a cut-off answer, the reading of a body that has not arrived whole
// ends now. Go's server would otherwise keep the connection open while it // ends now. Go's server would otherwise keep the connection open while it
// reads the rest. modifyResponse sets afterStatus only when it passes the // reads the rest. A flush fails if the time to send the answer ran out, and
// app's answer on. Any refusal after that has cut the answer off, since a // every flush after a failed one fails too. ReverseProxy ignores the failure,
// refusal ends the request to the app. Any answer is cut off when the time to // but panics if it was still waiting for more of the app's answer; finish runs
// send it runs out: a flush then fails, and every flush after a failed one // either way. Before a status, a flush would send a 200; after a 101,
// fails too. ReverseProxy ignores the failure, but panics if it was still // ReverseProxy has taken over the connection.
// waiting for more of the app's answer; finish runs either way. Before a
// status, a flush would send a 200; after a 101, ReverseProxy has taken over
// the connection.
switch { switch {
case rq.out.afterStatus != nil && rq.refused.Load() != 0, case rq.out.status != 0 && rq.out.status != http.StatusSwitchingProtocols &&
rq.out.status != 0 && rq.out.status != http.StatusSwitchingProtocols && errors.Is(rq.rc.Flush(), os.ErrDeadlineExceeded):
errors.Is(rq.rc.Flush(), os.ErrDeadlineExceeded):
rq.stopPassingOnBody(time.Now()) rq.stopPassingOnBody(time.Now())
case rq.refused.Load() == 0: case rq.refused.Load() == 0:
rq.stopPassingOnBody(rq.requestDeadline()) rq.stopPassingOnBody(rq.requestDeadline())
@@ -560,30 +552,20 @@ func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) {
requestEnd := rq.endRequest() requestEnd := rq.endRequest()
rq.mu.Lock()
stopTimer(rq.requestTimer)
rq.mu.Unlock()
rq.startResponseTimer(requestEnd)
}
// startResponseTimer starts SWRP_MAX_RESPONSE_TIME_MS, which runs from
// requestEnd, unless it has started already.
func (rq *request) startResponseTimer(requestEnd time.Time) {
limit := rq.h.config.MaxResponseTime
if limit == 0 {
return
}
rq.mu.Lock() rq.mu.Lock()
defer rq.mu.Unlock() defer rq.mu.Unlock()
if rq.timersStopped || rq.responseTimer != nil { if rq.timersStopped {
return return
} }
rq.responseTimer = time.AfterFunc(time.Until(requestEnd.Add(limit)), stopTimer(rq.requestTimer)
rq.responseTimedOut)
limit := rq.h.config.MaxResponseTime
if limit > 0 {
rq.responseTimer = time.AfterFunc(time.Until(requestEnd.Add(limit)),
rq.responseTimedOut)
}
} }
// responseTimedOut is called when SWRP_MAX_RESPONSE_TIME_MS runs out before the // responseTimedOut is called when SWRP_MAX_RESPONSE_TIME_MS runs out before the
+154 -460
View File
@@ -1,23 +1,13 @@
// Package state keeps sww's state in JSON files, as the "Persistent // Package state keeps sww's state in JSON files in
// state" section of SPEC.md requires and docs/state-files.md lists them. // SWW_STATE_DIR, as the "Persistent state" section of SPEC.md requires
// // and docs/state-files.md lists them:
// The state files are in SWW_STATE_DIR: // bans.json holds the bans, clients.json each client's counters and
// - bans.json holds the bans; // history, lookups.json GeoJS's answers, reputation.json the last try and
// - clients.json holds each client's counters and history; // last good copy of each list fetched from a URL, the DNSBL zones'
// - abuseipdb-checks-spent.json holds the AbuseIPDB checks spent today; // verdicts, and AbuseIPDB's scores and checks spent, and alerts.json the
// - alerts.json holds the cooldowns, the hour under way, the alerts // cooldowns, the hour under way, the alerts waiting for each destination
// waiting for each destination and the anomaly counters. // and the anomaly counters. Load
// // reads them at start, Watch takes in an admin's edit of one while
// The cache files are in SWW_CACHE_DIR. Each keeps answers from an
// outside service, which are fetched again when the file is lost:
// - lookups.json holds GeoJS's answers;
// - lists.json holds the last try and last good copy of each list
// fetched from a URL;
// - dnsbl.json holds the DNSBL zones' verdicts;
// - abuseipdb-scores.json holds AbuseIPDB's scores;
// - routewatch.json holds routewatch's answers.
//
// Load reads them at start, Watch takes in an admin's edit of one while
// sww runs, and Run and WriteAll write them. The disk is read and // sww runs, and Run and WriteAll write them. The disk is read and
// written outside the parts' locks, which are held only to take a // written outside the parts' locks, which are held only to take a
// snapshot or to put in what a file holds, so that no request waits on // snapshot or to put in what a file holds, so that no request waits on
@@ -41,7 +31,6 @@ import (
"sync" "sync"
"time" "time"
"cloud.google.com/go/civil"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/anomaly" "sneak.berlin/go/smallwebwaf/internal/anomaly"
@@ -50,7 +39,6 @@ import (
"sneak.berlin/go/smallwebwaf/internal/metrics" "sneak.berlin/go/smallwebwaf/internal/metrics"
"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/routewatch"
) )
// version is the version of the files' format, the only one read. // version is the version of the files' format, the only one read.
@@ -60,42 +48,15 @@ const version = 1
// hold visitors' addresses. // hold visitors' addresses.
const fileMode = 0o600 const fileMode = 0o600
// The state files' names, in Dir. // The state files' names.
const ( const (
bansJSON = "bans.json" bansJSON = "bans.json"
clientsJSON = "clients.json" clientsJSON = "clients.json"
abuseIPDBChecksSpentJSON = "abuseipdb-checks-spent.json" lookupsJSON = "lookups.json"
alertsJSON = "alerts.json" reputationJSON = "reputation.json"
alertsJSON = "alerts.json"
) )
// The cache files' names, in CacheDir.
const (
lookupsJSON = "lookups.json"
listsJSON = "lists.json"
dnsblJSON = "dnsbl.json"
abuseIPDBScoresJSON = "abuseipdb-scores.json"
routeWatchJSON = "routewatch.json"
)
// fileNames returns the names of the state files, then those of the
// cache files, in the order they are written.
func fileNames() []string {
return []string{
bansJSON, clientsJSON, abuseIPDBChecksSpentJSON, alertsJSON,
lookupsJSON, listsJSON, dnsblJSON, abuseIPDBScoresJSON, routeWatchJSON,
}
}
// isCacheFile reports whether name is the name of a cache file.
func isCacheFile(name string) bool {
switch name {
case lookupsJSON, listsJSON, dnsblJSON, abuseIPDBScoresJSON, routeWatchJSON:
return true
default:
return false
}
}
var ( var (
errVersion = errors.New("unknown version") errVersion = errors.New("unknown version")
// errMissing is for an entry without a field it needs. // errMissing is for an entry without a field it needs.
@@ -111,29 +72,24 @@ var (
// Params are what Load needs. // Params are what Load needs.
type Params struct { type Params struct {
// Dir is the directory of the state files (SWW_STATE_DIR), and // Dir is the directory of the state files (SWW_STATE_DIR).
// CacheDir that of the cache files (SWW_CACHE_DIR). Dir string
Dir string
CacheDir string
// WriteDelay is how long after a ban is made bans.json is written // WriteDelay is how long after a ban is made bans.json is written
// (SWW_STATE_WRITE_DELAY), and CounterInterval how often every file // (SWW_STATE_WRITE_DELAY), and CounterInterval how often every file
// is (SWW_STATE_COUNTER_INTERVAL). // is (SWW_STATE_COUNTER_INTERVAL).
WriteDelay time.Duration WriteDelay time.Duration
CounterInterval time.Duration CounterInterval time.Duration
// Ledger, Limiter, GeoJS, Lists, DNSBL, AbuseIPDB, RouteWatch, Alerts and // Ledger, Limiter, GeoJS, Lists, DNSBL, AbuseIPDB, Alerts and Anomalies
// Anomalies hold the state. RouteWatch is nil while SWW_ROUTEWATCH_URL is // hold the state. Alerts also receive a file_error alert for an edit set
// unset, and routewatch.json then holds no answers. Alerts also receive a // aside, and for a write that fails while sww runs.
// file_error alert for an edit set aside, and for a write that fails while Ledger *bans.Ledger
// sww runs. Limiter *ratelimit.Limiter
Ledger *bans.Ledger GeoJS *lookup.GeoJS
Limiter *ratelimit.Limiter Lists *reputation.Lists
GeoJS *lookup.GeoJS DNSBL *reputation.DNSBL
Lists *reputation.Lists AbuseIPDB *reputation.AbuseIPDB
DNSBL *reputation.DNSBL Alerts *alerts.Queue
AbuseIPDB *reputation.AbuseIPDB Anomalies *anomaly.Counters
RouteWatch *routewatch.Client
Alerts *alerts.Queue
Anomalies *anomaly.Counters
// Now tells the time by which the counters' buckets run out, normally // Now tells the time by which the counters' buckets run out, normally
// time.Now in UTC. // time.Now in UTC.
Now func() time.Time Now func() time.Time
@@ -145,7 +101,7 @@ type Params struct {
Metrics *metrics.Metrics Metrics *metrics.Metrics
} }
// Files are the state and cache files of a running sww. // Files are the state files of a running sww.
type Files struct { type Files struct {
params Params params Params
@@ -191,38 +147,13 @@ type lookupsFile struct {
Lookups []lookup.Answer `json:"lookups"` Lookups []lookup.Answer `json:"lookups"`
} }
// abuseIPDBChecksSpentFile is abuseipdb-checks-spent.json, indented for an // reputationFile is reputation.json, indented for an admin to read and
// admin to read and edit. // edit, so that each line of a list's copy is on a line of its own.
type abuseIPDBChecksSpentFile struct { type reputationFile struct {
Version int `json:"version"` Version int `json:"version"`
Day civil.Date `json:"day,omitzero"` Lists []reputation.List `json:"lists"`
Spent int `json:"spent"` Verdicts []reputation.Verdict `json:"verdicts"`
} AbuseIPDB reputation.Checks `json:"abuseipdb"`
// listsFile is lists.json, indented for an admin to read and edit, so
// that each line of a list's copy is on a line of its own.
type listsFile struct {
Version int `json:"version"`
Lists []reputation.List `json:"lists"`
}
// dnsblFile is dnsbl.json, with each verdict on a line of its own.
type dnsblFile struct {
Version int `json:"version"`
Verdicts []reputation.Verdict `json:"verdicts"`
}
// abuseIPDBScoresFile is abuseipdb-scores.json, with each score on a line
// of its own.
type abuseIPDBScoresFile struct {
Version int `json:"version"`
Scores []reputation.Score `json:"scores"`
}
// routeWatchFile is routewatch.json, with each answer on a line of its own.
type routeWatchFile struct {
Version int `json:"version"`
Answers []routewatch.Answer `json:"answers"`
} }
// alertsFile is alerts.json, indented for an admin to read and edit. // alertsFile is alerts.json, indented for an admin to read and edit.
@@ -247,45 +178,34 @@ type stateFile interface {
toUTC() toUTC()
} }
// Load checks that files can be written in Dir and CacheDir, and reads the // Load checks that files can be written in Dir, and reads the state files
// files in them into the parts of Params that hold the state. A missing // in it into the parts of Params that hold the state. A missing file is
// file is empty state, as on a first start. A state file that does not // empty state, as on a first start. A file that does not parse, has an
// parse, has an unknown version, or has an entry without a field it // unknown version, or has an entry without a field it needs, is an error
// needs, is an error that names the file and, where the JSON decoder // that names the file and, where the JSON decoder tells it, the line and
// tells it, the line and column, or else the entry. A cache file is never // column, or else the entry.
// an error, as readCache describes.
func Load(params Params) (*Files, error) { func Load(params Params) (*Files, error) {
err := checkWritable(params.Dir) err := checkWritable(params.Dir)
if err != nil { if err != nil {
return nil, fmt.Errorf("SWW_STATE_DIR cannot be written: %w", err) return nil, fmt.Errorf("SWW_STATE_DIR cannot be written: %w", err)
} }
err = checkWritable(params.CacheDir)
if err != nil {
return nil, fmt.Errorf("SWW_CACHE_DIR cannot be written: %w", err)
}
f := &Files{params: params, sums: map[string][sha256.Size]byte{}} f := &Files{params: params, sums: map[string][sha256.Size]byte{}}
bansRead, bansErr := f.read(bansJSON) bansRead, bansErr := f.read(bansJSON)
clientsRead, clientsErr := f.read(clientsJSON) clientsRead, clientsErr := f.read(clientsJSON)
_, checksErr := f.read(abuseIPDBChecksSpentJSON) lookupsRead, lookupsErr := f.read(lookupsJSON)
reputationRead, reputationErr := f.read(reputationJSON)
alertsRead, alertsErr := f.read(alertsJSON) alertsRead, alertsErr := f.read(alertsJSON)
err = errors.Join(bansErr, clientsErr, checksErr, alertsErr) err = errors.Join(bansErr, clientsErr, lookupsErr, reputationErr, alertsErr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lookupsRead := f.readCache(lookupsJSON) params.ProcessLog.Info("read the state files", "directory", params.Dir,
listsRead := f.readCache(listsJSON) "bans", bansRead, "clients", clientsRead, "lookups", lookupsRead,
f.readCache(dnsblJSON) "lists", reputationRead, "alerts_waiting", alertsRead)
f.readCache(abuseIPDBScoresJSON)
f.readCache(routeWatchJSON)
params.ProcessLog.Info("read the state and cache files", "directory", params.Dir,
"cache_directory", params.CacheDir, "bans", bansRead, "clients", clientsRead,
"lookups", lookupsRead, "lists", listsRead, "alerts_waiting", alertsRead)
return f, nil return f, nil
} }
@@ -314,101 +234,69 @@ func (f *Files) Run(ctx context.Context) {
f.logFailure(bansJSON, f.writeFile(bansJSON)) f.logFailure(bansJSON, f.writeFile(bansJSON))
case <-interval.C: case <-interval.C:
for _, name := range fileNames() { for _, name := range []string{
bansJSON, clientsJSON, lookupsJSON, reputationJSON, alertsJSON,
} {
f.logFailure(name, f.writeFile(name)) f.logFailure(name, f.writeFile(name))
} }
} }
} }
} }
// WriteAll writes every state and cache file, as sww stops. A file that // WriteAll writes every state file, as sww stops. A file that
// fails does not keep the others from being written. // fails does not keep the others from being written.
func (f *Files) WriteAll() error { func (f *Files) WriteAll() error {
names := fileNames() return errors.Join(f.writeFile(bansJSON), f.writeFile(clientsJSON),
errs := make([]error, 0, len(names)) f.writeFile(lookupsJSON), f.writeFile(reputationJSON), f.writeFile(alertsJSON))
for _, name := range names {
errs = append(errs, f.writeFile(name))
}
return errors.Join(errs...)
} }
// Watch watches Dir and CacheDir until ctx is done, and takes in an // Watch watches Dir until ctx is done, and takes in an admin's edit of a
// admin's edit of a file as soon as it is saved: what the file holds // state file as soon as it is saved: what the file holds replaces what
// replaces what sww held for it. An edit that does not parse is left for // sww held for it. An edit that does not parse is left for the
// the file's next write, which sets it aside, since a file can be read // file's next write, which sets it aside, since a file can be read while
// while an editor is still writing it. A directory that cannot be watched // an editor is still writing it. If Dir cannot be watched, that is
// is logged, and the other is still watched. An edit in a directory not // logged, and an edit is taken in only before its file is written.
// watched is taken in only before its file is written.
func (f *Files) Watch(ctx context.Context) { func (f *Files) Watch(ctx context.Context) {
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err == nil {
defer func() {
_ = watcher.Close()
}()
err = watcher.Add(f.params.Dir)
}
if err != nil { if err != nil {
f.params.ProcessLog.Error("cannot watch the state and cache files for edits", f.params.ProcessLog.Error("cannot watch the state files for edits",
"error", err.Error()) "error", err.Error())
return return
} }
defer func() { f.params.ProcessLog.Info("watching the state files for edits",
_ = watcher.Close() "directory", f.params.Dir)
}()
var watched []string
for _, dir := range []struct{ path, failed string }{
{f.params.Dir, "cannot watch the state files for edits"},
{f.params.CacheDir, "cannot watch the cache files for edits"},
} {
err = watcher.Add(dir.path)
if err != nil {
f.params.ProcessLog.Error(dir.failed, "directory", dir.path,
"error", err.Error())
continue
}
watched = append(watched, dir.path)
}
watching := "watching the state and cache files for edits"
switch {
case len(watched) == 0:
return
case len(watched) == 1 && watched[0] == f.params.Dir:
watching = "watching the state files for edits"
case len(watched) == 1:
watching = "watching the cache files for edits"
}
f.params.ProcessLog.Info(watching, "directories", watched)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case event := <-watcher.Events: case event := <-watcher.Events:
name := filepath.Base(event.Name) switch name := filepath.Base(event.Name); name {
if slices.Contains(fileNames(), name) && case bansJSON, clientsJSON, lookupsJSON, reputationJSON, alertsJSON:
filepath.Join(f.dir(name), name) == event.Name {
f.fileChanged(name) f.fileChanged(name)
} }
case err = <-watcher.Errors: case err = <-watcher.Errors:
f.params.ProcessLog.Warn("watching the state and cache files failed", f.params.ProcessLog.Warn("watching the state files failed",
"error", err.Error()) "error", err.Error())
} }
} }
} }
// logFailure logs a write of the file name that failed, and raises // logFailure logs a write of the state file name that failed, and raises
// a file_error alert for it. // a file_error alert for it.
func (f *Files) logFailure(name string, err error) { func (f *Files) logFailure(name string, err error) {
if err != nil { if err != nil {
failed := "writing the state files failed" const failed = "writing the state files failed"
if isCacheFile(name) {
failed = "writing the cache files failed"
}
// Raised before it is logged, so that the alert is there once the // Raised before it is logged, so that the alert is there once the
// log line is. // log line is.
@@ -416,14 +304,14 @@ func (f *Files) logFailure(name string, err error) {
Event: alerts.EventFileError, Event: alerts.EventFileError,
Reason: failed, Reason: failed,
Detail: map[string]any{ Detail: map[string]any{
"file": filepath.Join(f.dir(name), name), "error": err.Error(), "file": filepath.Join(f.params.Dir, name), "error": err.Error(),
}, },
}) })
f.params.ProcessLog.Error(failed, "error", err.Error()) f.params.ProcessLog.Error(failed, "error", err.Error())
} }
} }
// fileChanged takes in what the file name holds, as Watch sees it // fileChanged takes in what the state file name holds, as Watch sees it
// change, if that is an edit made since sww last read or wrote // change, if that is an edit made since sww last read or wrote
// the file. A file that cannot be read or does not parse is left for its // the file. A file that cannot be read or does not parse is left for its
// next write. // next write.
@@ -439,7 +327,7 @@ func (f *Files) fileChanged(name string) {
_ = f.takeInEdit(name, data) _ = f.takeInEdit(name, data)
} }
// takeInEdit takes in data, an edit of the file name, as takeIn // takeInEdit takes in data, an edit of the state file name, as takeIn
// does, and counts and logs it. Every edit taken in while sww // does, and counts and logs it. Every edit taken in while sww
// runs, by Watch or by a write, is taken in here. An edit that does not // runs, by Watch or by a write, is taken in here. An edit that does not
// parse is neither counted nor logged, and takeIn's error returned. // parse is neither counted nor logged, and takeIn's error returned.
@@ -449,31 +337,17 @@ func (f *Files) takeInEdit(name string, data []byte) error {
return err return err
} }
tookIn := "took in an edit of a state file"
if isCacheFile(name) {
tookIn = "took in an edit of a cache file"
}
// Counted before it is logged, so that the count is there once the // Counted before it is logged, so that the count is there once the
// log line is. // log line is.
f.params.Metrics.StateFileEditTakenIn(name) f.params.Metrics.StateFileEditTakenIn(name)
f.params.ProcessLog.Info(tookIn, "file", filepath.Join(f.dir(name), name)) f.params.ProcessLog.Info("took in an edit of a state file",
"file", filepath.Join(f.params.Dir, name))
return nil return nil
} }
// dir returns the directory of the file name: CacheDir for a cache file, // read takes in the state file name at start, and returns how many
// and Dir for a state file. // entries it holds. A missing file holds none.
func (f *Files) dir(name string) string {
if isCacheFile(name) {
return f.params.CacheDir
}
return f.params.Dir
}
// read takes in the file name at start, and returns how many entries it
// holds. A missing file holds none.
func (f *Files) read(name string) (int, error) { func (f *Files) read(name string) (int, error) {
data, changed, err := f.readChanged(name) data, changed, err := f.readChanged(name)
if err != nil || !changed { if err != nil || !changed {
@@ -483,44 +357,14 @@ func (f *Files) read(name string) (int, error) {
return f.takeIn(name, data, false) return f.takeIn(name, data, false)
} }
// readCache takes in the cache file name at start, as read does. A cache // readChanged returns what the state file name holds, and whether that
// file that cannot be read, or does not parse, does not stop the start: // has changed since sww last read or wrote the file, as it has
// it is logged, and its cache starts empty and fills again. One that does // for a file sww never read or wrote. A missing file has not
// not parse is set aside as an edit is, and one that cannot be read is // changed: it is written again at its next write.
// written over at its next write.
func (f *Files) readCache(name string) int {
data, changed, err := f.readChanged(name)
if err != nil || !changed {
f.logUnread(name, err)
return 0
}
entries, err := f.takeIn(name, data, false)
if err != nil {
f.logUnread(name, f.setAside(name, err))
}
return entries
}
// logUnread logs err, the reason the cache file name was not read at
// start, unless it is nil.
func (f *Files) logUnread(name string, err error) {
if err != nil {
f.params.ProcessLog.Warn("cannot read a cache file, so its cache starts empty",
"file", filepath.Join(f.dir(name), name), "error", err.Error())
}
}
// readChanged returns what the file name holds, and whether that has
// changed since sww last read or wrote the file, as it has for a file
// sww never read or wrote. A missing file has not changed: it is written
// again at its next write.
func (f *Files) readChanged(name string) ([]byte, bool, error) { func (f *Files) readChanged(name string) ([]byte, bool, error) {
path := filepath.Join(f.dir(name), name) path := filepath.Join(f.params.Dir, name)
data, err := os.ReadFile(path) //nolint:gosec // a file in one of the two directories data, err := os.ReadFile(path) //nolint:gosec // a state file, in SWW_STATE_DIR
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
return nil, false, nil return nil, false, nil
} }
@@ -532,39 +376,15 @@ func (f *Files) readChanged(name string) ([]byte, bool, error) {
return data, sha256.Sum256(data) != f.sums[name], nil return data, sha256.Sum256(data) != f.sums[name], nil
} }
// takeIn parses data, what the file name holds, puts it into the // takeIn parses data, what the state file name holds, puts it into the
// part that keeps that state, in place of what the part held, and returns // part that keeps that state, in place of what the part held, and returns
// how many entries the file holds. edit is whether data is an admin's // how many entries the file holds. edit is whether data is an admin's
// edit taken in while sww runs, rather than the file read at the // edit taken in while sww runs, rather than the file read at the
// start. An error names the file and, where the JSON decoder tells it, // start. An error names the file and, where the JSON decoder tells it,
// the line and column, or else the entry. abuseipdb-checks-spent.json // the line and column, or else the entry.
// holds no entries.
func (f *Files) takeIn(name string, data []byte, edit bool) (int, error) { func (f *Files) takeIn(name string, data []byte, edit bool) (int, error) {
path := filepath.Join(f.dir(name), name) path := filepath.Join(f.params.Dir, name)
var (
entries int
err error
)
if isCacheFile(name) {
entries, err = f.takeInCache(name, path, data)
} else {
entries, err = f.takeInState(name, path, data, edit)
}
if err != nil {
return 0, err
}
f.sums[name] = sha256.Sum256(data)
return entries, nil
}
// takeInState parses data, what the state file name, at path, holds, and
// puts it in, as takeIn describes.
func (f *Files) takeInState(name, path string, data []byte, edit bool) (int, error) {
var entries int var entries int
switch name { switch name {
@@ -598,33 +418,6 @@ func (f *Files) takeInState(name, path string, data []byte, edit bool) (int, err
f.params.Limiter.Load(file.Clients, f.params.Now()) f.params.Limiter.Load(file.Clients, f.params.Now())
entries = len(file.Clients) entries = len(file.Clients)
case abuseIPDBChecksSpentJSON:
var file abuseIPDBChecksSpentFile
err := parse(path, data, &file)
if err != nil {
return 0, err
}
f.params.AbuseIPDB.LoadChecksSpent(reputation.ChecksSpent{
Day: file.Day, Spent: file.Spent,
})
case alertsJSON:
waiting, err := f.takeInAlerts(path, data)
if err != nil {
return 0, err
}
entries = waiting
}
return entries, nil
}
// takeInCache parses data, what the cache file name, at path, holds, and
// puts it in, as takeIn describes.
func (f *Files) takeInCache(name, path string, data []byte) (int, error) {
switch name {
case lookupsJSON: case lookupsJSON:
var file lookupsFile var file lookupsFile
@@ -634,10 +427,9 @@ func (f *Files) takeInCache(name, path string, data []byte) (int, error) {
} }
f.params.GeoJS.Load(file.Lookups) f.params.GeoJS.Load(file.Lookups)
entries = len(file.Lookups)
return len(file.Lookups), nil case reputationJSON:
case listsJSON: var file reputationFile
var file listsFile
err := parse(path, data, &file) err := parse(path, data, &file)
if err != nil { if err != nil {
@@ -649,43 +441,21 @@ func (f *Files) takeInCache(name, path string, data []byte) (int, error) {
return 0, fmt.Errorf("%s: %w", path, err) return 0, fmt.Errorf("%s: %w", path, err)
} }
return len(file.Lists), nil
case dnsblJSON:
var file dnsblFile
err := parse(path, data, &file)
if err != nil {
return 0, err
}
f.params.DNSBL.Load(file.Verdicts) f.params.DNSBL.Load(file.Verdicts)
f.params.AbuseIPDB.Load(file.AbuseIPDB)
return len(file.Verdicts), nil entries = len(file.Lists)
case routeWatchJSON: case alertsJSON:
var file routeWatchFile waiting, err := f.takeInAlerts(path, data)
err := parse(path, data, &file)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if f.params.RouteWatch != nil { entries = waiting
f.params.RouteWatch.Load(file.Answers)
}
return len(file.Answers), nil
default: // abuseipdb-scores.json
var file abuseIPDBScoresFile
err := parse(path, data, &file)
if err != nil {
return 0, err
}
f.params.AbuseIPDB.Load(file.Scores)
return len(file.Scores), nil
} }
f.sums[name] = sha256.Sum256(data)
return entries, nil
} }
// takeInAlerts parses data, what alerts.json, at path, holds, puts it // takeInAlerts parses data, what alerts.json, at path, holds, puts it
@@ -723,22 +493,17 @@ func (f *Files) takeInAlerts(path string, data []byte) (int, error) {
return entries, nil return entries, nil
} }
// writeFile writes the file name from what sww holds. An edit made since // writeFile writes the state file name from what sww holds. An
// sww last read or wrote the file is taken in first, so that it is not // edit made since sww last read or wrote the file is taken in
// overwritten, or set aside if it does not parse. A state file that // first, so that it is not overwritten, or set aside if it does not
// cannot be read, or an edit that cannot be set aside, is left as it is, // parse. A file that cannot be read, or an edit that cannot be set
// and the write given up. A cache file that cannot be read is written // aside, is left as it is, and the write given up. Every write is counted
// over, since what it held is fetched again. Every write is counted in // in the metrics, and one that fails or is given up as a failure.
// the metrics, and one that fails or is given up as a failure.
func (f *Files) writeFile(name string) error { func (f *Files) writeFile(name string) error {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
data, changed, err := f.readChanged(name) data, changed, err := f.readChanged(name)
if err != nil && isCacheFile(name) {
err = nil
}
if err == nil && changed { if err == nil && changed {
err = f.takeInEdit(name, data) err = f.takeInEdit(name, data)
if err != nil { if err != nil {
@@ -754,7 +519,7 @@ func (f *Files) writeFile(name string) error {
} }
if err == nil { if err == nil {
err = write(f.dir(name), name, data) err = write(f.params.Dir, name, data)
} }
if err == nil { if err == nil {
@@ -762,7 +527,7 @@ func (f *Files) writeFile(name string) error {
// fails, so that its next read does not take it for an admin's // fails, so that its next read does not take it for an admin's
// edit. // edit.
f.sums[name] = sha256.Sum256(data) f.sums[name] = sha256.Sum256(data)
err = syncDirectory(f.dir(name)) err = syncDirectory(f.params.Dir)
} }
f.params.Metrics.StateFileWritten(name, len(data), err) f.params.Metrics.StateFileWritten(name, len(data), err)
@@ -770,23 +535,20 @@ func (f *Files) writeFile(name string) error {
return err return err
} }
// setAside renames the file name, an edit that does not parse with // setAside renames the state file name, an edit that does not parse with
// parseErr, to name.bad, for the admin to mend, and logs it with where in // parseErr, to name.bad, for the admin to mend, and logs it with where in
// the file the error is, and raises a file_error alert for it. If the // the file the error is, and raises a file_error alert for it. If the
// rename fails, the edit is left as it is, and the error returned is // rename fails, the edit is left as it is, and the error returned is
// parseErr joined with the rename's. // parseErr joined with the rename's.
func (f *Files) setAside(name string, parseErr error) error { func (f *Files) setAside(name string, parseErr error) error {
path := filepath.Join(f.dir(name), name) path := filepath.Join(f.params.Dir, name)
err := os.Rename(path, path+".bad") err := os.Rename(path, path+".bad")
if err != nil { if err != nil {
return errors.Join(parseErr, err) return errors.Join(parseErr, err)
} }
setAside := "set aside an edit of a state file that does not parse" const setAside = "set aside an edit of a state file that does not parse"
if isCacheFile(name) {
setAside = "set aside an edit of a cache file that does not parse"
}
// Raised before it is logged, so that the alert is there once the log // Raised before it is logged, so that the alert is there once the log
// line is. // line is.
@@ -801,7 +563,7 @@ func (f *Files) setAside(name string, parseErr error) error {
return nil return nil
} }
// encode returns the file name as sww writes it, from a // encode returns the state file name as sww writes it, from a
// snapshot of the part that keeps that state. // snapshot of the part that keeps that state.
func (f *Files) encode(name string) ([]byte, error) { func (f *Files) encode(name string) ([]byte, error) {
switch name { switch name {
@@ -813,24 +575,10 @@ func (f *Files) encode(name string) ([]byte, error) {
return encodeOnePerLine("clients", f.params.Limiter.Snapshot()) return encodeOnePerLine("clients", f.params.Limiter.Snapshot())
case lookupsJSON: case lookupsJSON:
return encodeOnePerLine("lookups", f.params.GeoJS.Snapshot()) return encodeOnePerLine("lookups", f.params.GeoJS.Snapshot())
case listsJSON: case reputationJSON:
return encodeIndented(listsFile{Version: version, Lists: f.params.Lists.Snapshot()}) return encodeIndented(reputationFile{
case dnsblJSON: Version: version, Lists: f.params.Lists.Snapshot(),
return encodeOnePerLine("verdicts", f.params.DNSBL.Snapshot()) Verdicts: f.params.DNSBL.Snapshot(), AbuseIPDB: f.params.AbuseIPDB.Snapshot(),
case abuseIPDBScoresJSON:
return encodeOnePerLine("scores", f.params.AbuseIPDB.Snapshot())
case routeWatchJSON:
var answers []routewatch.Answer
if f.params.RouteWatch != nil {
answers = f.params.RouteWatch.Snapshot()
}
return encodeOnePerLine("answers", answers)
case abuseIPDBChecksSpentJSON:
checks := f.params.AbuseIPDB.ChecksSpent()
return encodeIndented(abuseIPDBChecksSpentFile{
Version: version, Day: checks.Day, Spent: checks.Spent,
}) })
default: // alerts.json default: // alerts.json
held := f.params.Alerts.Snapshot() held := f.params.Alerts.Snapshot()
@@ -1000,8 +748,14 @@ func (f *lookupsFile) check(data []byte) error {
// check refuses a list without its URL, which would name no list, or the // check refuses a list without its URL, which would name no list, or the
// time it was last tried, which would have it fetched at once, and a copy // time it was last tried, which would have it fetched at once, and a copy
// of it without the time it was fetched, or without its lines, which hold // of it without the time it was fetched, or without its lines, which hold
// the list. // the list. It refuses a verdict without its zone or its client, which
func (f *listsFile) check([]byte) error { // would be about no one, whether the zone lists the client, or the time
// it was fetched, which would drop it, and so an AbuseIPDB score without
// its client, the score, or the time it was fetched. A verdict's listed is
// false for a client the zone does not list, and a score can be 0, which
// the structs cannot tell from a missing one, so each is read again as
// written.
func (f *reputationFile) check(data []byte) error {
for i, kept := range f.Lists { for i, kept := range f.Lists {
switch { switch {
case kept.URL == "": case kept.URL == "":
@@ -1015,15 +769,6 @@ func (f *listsFile) check([]byte) error {
} }
} }
return nil
}
// check refuses a verdict without its zone or its client, which would be
// about no one, whether the zone lists the client, or the time it was
// fetched, which would drop it. A verdict's listed is false for a client
// the zone does not list, which Verdict cannot tell from a missing one,
// so each listed is read again as written.
func (f *dnsblFile) check(data []byte) error {
var written struct { var written struct {
Verdicts []struct { Verdicts []struct {
Listed *bool `json:"listed"` Listed *bool `json:"listed"`
@@ -1038,27 +783,28 @@ func (f *dnsblFile) check(data []byte) error {
for i, verdict := range f.Verdicts { for i, verdict := range f.Verdicts {
switch { switch {
case verdict.Zone == "": case verdict.Zone == "":
return missing(i, "zone") return fmt.Errorf("verdicts %w", missing(i, "zone"))
case !verdict.Client.IsValid(): case !verdict.Client.IsValid():
return missing(i, "client") return fmt.Errorf("verdicts %w", missing(i, "client"))
case written.Verdicts[i].Listed == nil: case written.Verdicts[i].Listed == nil:
return missing(i, "listed") return fmt.Errorf("verdicts %w", missing(i, "listed"))
case verdict.Fetched.IsZero(): case verdict.Fetched.IsZero():
return missing(i, "fetched") return fmt.Errorf("verdicts %w", missing(i, "fetched"))
} }
} }
return nil return checkScores(f.AbuseIPDB.Scores, data)
} }
// check refuses an AbuseIPDB score without its client, the score, or the // checkScores refuses an AbuseIPDB score, of scores, read from data, as
// time it was fetched. A score can be 0, which Score cannot tell from a // reputationFile's check describes.
// missing one, so each score is read again as written. func checkScores(scores []reputation.Score, data []byte) error {
func (f *abuseIPDBScoresFile) check(data []byte) error {
var written struct { var written struct {
Scores []struct { AbuseIPDB struct {
Score *int64 `json:"score"` Scores []struct {
} `json:"scores"` Score *int64 `json:"score"`
} `json:"scores"`
} `json:"abuseipdb"`
} }
err := json.Unmarshal(data, &written) err := json.Unmarshal(data, &written)
@@ -1066,57 +812,20 @@ func (f *abuseIPDBScoresFile) check(data []byte) error {
return err return err
} }
for i, kept := range f.Scores { for i, kept := range scores {
switch { switch {
case !kept.Client.IsValid(): case !kept.Client.IsValid():
return missing(i, "client") return fmt.Errorf("abuseipdb scores %w", missing(i, "client"))
case written.Scores[i].Score == nil: case written.AbuseIPDB.Scores[i].Score == nil:
return missing(i, "score") return fmt.Errorf("abuseipdb scores %w", missing(i, "score"))
case kept.Fetched.IsZero(): case kept.Fetched.IsZero():
return missing(i, "fetched") return fmt.Errorf("abuseipdb scores %w", missing(i, "fetched"))
} }
} }
return nil return nil
} }
// check refuses an answer without its client, which would answer for
// nobody, its netblock, or the time routewatch gave it, which would drop
// it. "" is the netblock of an address without a live route, which Answer
// cannot tell from a missing one, so each netblock is read again as
// written.
func (f *routeWatchFile) check(data []byte) error {
var written struct {
Answers []struct {
Netblock *string `json:"netblock"`
} `json:"answers"`
}
err := json.Unmarshal(data, &written)
if err != nil {
return err
}
for i, answer := range f.Answers {
switch {
case !answer.Client.IsValid():
return missing(i, "client")
case written.Answers[i].Netblock == nil:
return missing(i, "netblock")
case answer.Answered.IsZero():
return missing(i, "answered")
}
}
return nil
}
// check refuses nothing, since a missing day or count means no check
// spent.
func (f *abuseIPDBChecksSpentFile) check([]byte) error {
return nil
}
// check refuses a cooldown without its event or when its alert was sent, // check refuses a cooldown without its event or when its alert was sent,
// which would hold back no repeat, alerts waiting for a destination with // which would hold back no repeat, alerts waiting for a destination with
// another name than webhook, slack or ntfy, most likely misspelt, an // another name than webhook, slack or ntfy, most likely misspelt, an
@@ -1238,37 +947,22 @@ func (f *lookupsFile) toUTC() {
} }
} }
// toUTC converts both times of each list to UTC. // toUTC converts every time of each list, verdict and AbuseIPDB score to
func (f *listsFile) toUTC() { // UTC.
func (f *reputationFile) toUTC() {
for i := range f.Lists { for i := range f.Lists {
convertToUTC(&f.Lists[i].Tried, &f.Lists[i].Fetched) convertToUTC(&f.Lists[i].Tried, &f.Lists[i].Fetched)
} }
}
// toUTC converts the time of each verdict to UTC.
func (f *dnsblFile) toUTC() {
for i := range f.Verdicts { for i := range f.Verdicts {
convertToUTC(&f.Verdicts[i].Fetched) convertToUTC(&f.Verdicts[i].Fetched)
} }
}
// toUTC converts the time of each AbuseIPDB score to UTC. for i := range f.AbuseIPDB.Scores {
func (f *abuseIPDBScoresFile) toUTC() { convertToUTC(&f.AbuseIPDB.Scores[i].Fetched)
for i := range f.Scores {
convertToUTC(&f.Scores[i].Fetched)
} }
} }
// toUTC converts the time of each answer to UTC.
func (f *routeWatchFile) toUTC() {
for i := range f.Answers {
convertToUTC(&f.Answers[i].Answered)
}
}
// toUTC does nothing: abuseipdb-checks-spent.json holds a day, not a time.
func (f *abuseIPDBChecksSpentFile) toUTC() {}
// toUTC converts every time of each cooldown, of the hour, of each alert // toUTC converts every time of each cooldown, of the hour, of each alert
// waiting and of each anomaly counter to UTC. // waiting and of each anomaly counter to UTC.
func (f *alertsFile) toUTC() { func (f *alertsFile) toUTC() {
@@ -1420,7 +1114,7 @@ func write(dir, name string, data []byte) error {
// syncDirectory syncs dir to the disk, so that a rename in it lasts. // syncDirectory syncs dir to the disk, so that a rename in it lasts.
func syncDirectory(dir string) error { func syncDirectory(dir string) error {
directory, err := os.Open(dir) //nolint:gosec // SWW_STATE_DIR or SWW_CACHE_DIR itself directory, err := os.Open(dir) //nolint:gosec // SWW_STATE_DIR itself
if err != nil { if err != nil {
return err return err
} }
@@ -1430,7 +1124,7 @@ func syncDirectory(dir string) error {
// writeSynced writes data to the file at path, and syncs it to the disk. // writeSynced writes data to the file at path, and syncs it to the disk.
func writeSynced(path string, data []byte) error { func writeSynced(path string, data []byte) error {
//nolint:gosec // a file's temporary file, in SWW_STATE_DIR or SWW_CACHE_DIR //nolint:gosec // a state file's temporary file, in SWW_STATE_DIR
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode) file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil { if err != nil {
return err return err
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -41,7 +41,7 @@ func TestHealthCheckInWAFsContainer(t *testing.T) {
exited <- run(ctx, settings, out) exited <- run(ctx, settings, out)
}() }()
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string) addr, _ := out.line(t, "msg", "starting")["address"].(string)
_, port, _ := net.SplitHostPort(addr) _, port, _ := net.SplitHostPort(addr)
// The container's settings: an address to listen on with an empty // The container's settings: an address to listen on with an empty
+35 -31
View File
@@ -71,8 +71,6 @@ func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
}, out) }, out)
}() }()
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string)
t.Cleanup(func() { t.Cleanup(func() {
stop() stop()
@@ -88,6 +86,8 @@ func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
out.line(t, "msg", "stopped") out.line(t, "msg", "stopped")
}) })
addr, _ := out.line(t, "msg", "starting")["address"].(string)
return "http://" + addr, out return "http://" + addr, out
} }
@@ -105,7 +105,7 @@ func startWAF(t *testing.T, env map[string]string) (string, *output, func()) {
exited <- run(ctx, env, out) exited <- run(ctx, env, out)
}() }()
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string) addr, _ := out.line(t, "msg", "starting")["address"].(string)
return addr, out, func() { return addr, out, func() {
stop() stop()
@@ -507,10 +507,9 @@ func TestAStatusWAFDoesNotTakeIsReportedAs599(t *testing.T) {
} }
// startRProxyProcess runs `sww rproxy` in a process of its own, with the // startRProxyProcess runs `sww rproxy` in a process of its own, with the
// settings in env, each as NAME=value. It returns where rproxy listens and a // settings in env, each as NAME=value. It returns where rproxy listens, the
// function that sends rproxy SIGTERM and fails the test unless it exits with // process, and what rproxy writes.
// status 0 within waitLimit. func startRProxyProcess(t *testing.T, env ...string) (string, *exec.Cmd, *output) {
func startRProxyProcess(t *testing.T, env ...string) (string, func()) {
t.Helper() t.Helper()
binary, err := os.Executable() binary, err := os.Executable()
@@ -518,8 +517,11 @@ func startRProxyProcess(t *testing.T, env ...string) (string, func()) {
t.Fatalf("find the test binary: %v", err) t.Fatalf("find the test binary: %v", err)
} }
ctx, cancel := context.WithTimeout(t.Context(), 3*waitLimit)
t.Cleanup(cancel)
out := &output{} out := &output{}
cmd := exec.CommandContext(t.Context(), binary) //nolint:gosec // this test binary cmd := exec.CommandContext(ctx, binary) //nolint:gosec // this test binary
cmd.Stdout = out cmd.Stdout = out
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
@@ -527,25 +529,19 @@ func startRProxyProcess(t *testing.T, env ...string) (string, func()) {
"SWRP_LISTEN_ADDR="+localhost+":0") "SWRP_LISTEN_ADDR="+localhost+":0")
cmd.Env = append(cmd.Env, env...) cmd.Env = append(cmd.Env, env...)
exited := startProcess(t, cmd) err = cmd.Start()
if err != nil {
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string) t.Fatalf("start: %v", err)
return addr, func() {
err := cmd.Process.Signal(syscall.SIGTERM)
if err != nil {
t.Fatalf("signal: %v", err)
}
select {
case status := <-exited:
if status != 0 {
t.Errorf("exit status %d, want 0; output:\n%s", status, out.text())
}
case <-time.After(waitLimit):
t.Error("still running after being told to stop")
}
} }
t.Cleanup(func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
})
addr, _ := out.line(t, "msg", "starting")["address"].(string)
return addr, cmd, out
} }
func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) { func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) {
@@ -576,7 +572,7 @@ func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) {
t.Fatalf("write the certificate: %v", err) t.Fatalf("write the certificate: %v", err)
} }
addr, _ := startRProxyProcess(t, addr, _, _ := startRProxyProcess(t,
wafBackend+"="+backend.URL, wafBackend+"="+backend.URL,
"SWRP_UPSTREAM_URL="+startApp(t), "SWRP_UPSTREAM_URL="+startApp(t),
"SSL_CERT_FILE="+certificates) "SSL_CERT_FILE="+certificates)
@@ -655,7 +651,7 @@ func TestRProxyStoppingWaitsForAReportBeingSent(t *testing.T) {
exited <- runRProxy(ctx, env, out) exited <- runRProxy(ctx, env, out)
}() }()
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string) addr, _ := out.line(t, "msg", "starting")["address"].(string)
wantStatus(t, "http://"+addr+"/", placed, http.StatusOK) wantStatus(t, "http://"+addr+"/", placed, http.StatusOK)
@@ -754,7 +750,7 @@ func TestRProxyStoppingCutsOffARequestAfterFiveSeconds(t *testing.T) {
exited <- runRProxy(ctx, env, out) exited <- runRProxy(ctx, env, out)
}() }()
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string) addr, _ := out.line(t, "msg", "starting")["address"].(string)
conn := sendAndKeepOpen(t, addr, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n") conn := sendAndKeepOpen(t, addr, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n")
select { select {
@@ -849,7 +845,7 @@ func TestMainRunsRProxy(t *testing.T) {
t.Parallel() t.Parallel()
// Nothing listens there; the health check does not ask. // Nothing listens there; the health check does not ask.
addr, stop := 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+waf.HealthPath, http.NoBody) "http://"+addr+waf.HealthPath, http.NoBody)
@@ -872,5 +868,13 @@ func TestMainRunsRProxy(t *testing.T) {
t.Errorf("health check answered %q (%v)", body, err) t.Errorf("health check answered %q (%v)", body, err)
} }
stop() err = cmd.Process.Signal(syscall.SIGTERM)
if err != nil {
t.Fatalf("signal: %v", err)
}
err = cmd.Wait()
if err != nil {
t.Errorf("exit: %v; output:\n%s", err, out.text())
}
} }
+14 -16
View File
@@ -1,9 +1,9 @@
// Package sww runs the sww process, as the command it is given. // Package sww runs the sww process, as the command it is given.
// `sww waf` reads the settings, the rule files, the lookup database // `sww waf` reads the settings, the rule files, the lookup database
// and the state and cache files, answers rproxy's questions until it is told to // and the state files, answers rproxy's questions until it is told to stop, and
// stop, and then stops in an orderly way, writing the state and cache files. // then stops in an orderly way, writing the state files. `sww rproxy`
// `sww rproxy` reads rproxy's settings alone, and runs the reverse proxy in // reads rproxy's settings alone, and runs the reverse proxy in front of one app
// front of one app that asks waf about each request. `sww healthcheck` is the // that asks waf about each request. `sww healthcheck` is the
// container's health check. // container's health check.
package sww package sww
@@ -101,8 +101,8 @@ func printUsage(w io.Writer) int {
} }
// RunWAF is `sww waf`: it reads the settings, the rule files, // RunWAF is `sww waf`: it reads the settings, the rule files,
// the lookup database and the state and cache files, then serves requests until // the lookup database and the state files, then serves requests until ctx is
// ctx is done. It returns the process's exit status, 1 when waf cannot start. // done. It returns the process's exit status, 1 when waf cannot start.
func RunWAF(ctx context.Context, params Params) int { func RunWAF(ctx context.Context, params Params) int {
// Until the settings are read, the one message is an invalid setting's // Until the settings are read, the one message is an invalid setting's
// error, which every SWW_LOG_LEVEL lets through. // error, which every SWW_LOG_LEVEL lets through.
@@ -171,7 +171,7 @@ func RunWAF(ctx context.Context, params Params) int {
files, err := loadStateFiles(cfg, server, alertQueue, now, processLog) files, err := loadStateFiles(cfg, server, alertQueue, now, processLog)
if err != nil { if err != nil {
processLog.Error("cannot use the state and cache files", "error", err.Error()) processLog.Error("cannot use the state files", "error", err.Error())
return 1 return 1
} }
@@ -236,15 +236,14 @@ func newServer(
return server, nil return server, nil
} }
// loadStateFiles reads the state and cache files into the parts of server and // loadStateFiles reads the state files into the parts of server and into
// into alertQueue, as state.Load does. // alertQueue, as state.Load does.
func loadStateFiles( func loadStateFiles(
cfg *config.Config, server *waf.Server, alertQueue *alerts.Queue, cfg *config.Config, server *waf.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{
Dir: cfg.StateDir, Dir: cfg.StateDir,
CacheDir: cfg.CacheDir,
WriteDelay: cfg.StateWriteDelay, WriteDelay: cfg.StateWriteDelay,
CounterInterval: cfg.StateCounterInterval, CounterInterval: cfg.StateCounterInterval,
Ledger: server.Ledger, Ledger: server.Ledger,
@@ -253,7 +252,6 @@ func loadStateFiles(
Lists: server.Lists, Lists: server.Lists,
DNSBL: server.DNSBL, DNSBL: server.DNSBL,
AbuseIPDB: server.AbuseIPDB, AbuseIPDB: server.AbuseIPDB,
RouteWatch: server.RouteWatch,
Alerts: alertQueue, Alerts: alertQueue,
Anomalies: server.Anomalies, Anomalies: server.Anomalies,
Now: now, Now: now,
@@ -319,13 +317,13 @@ func startSending(
} }
} }
// serve serves requests on listener, writes the state and cache files as they // serve serves requests on listener, writes the state files as they are
// are due, takes in an admin's edits of them, reads the rule files again as // due, takes in an admin's edits of them, reads the rule files again as
// they change, and the lookup database when it is replaced, fetches the // they change, and the lookup database when it is replaced, fetches the
// lists the settings name by URL as they are due, and sends the alerts, // lists the settings name by URL as they are due, and sends the alerts,
// until ctx is done. Then it gives the requests in progress // until ctx is done. Then it gives the requests in progress
// shutdownTimeout to finish, and writes every state and cache file, // shutdownTimeout to finish, and writes every state file, alerts.json with
// alerts.json with the alerts still waiting. // the alerts still waiting.
func serve( func serve(
ctx context.Context, server *waf.Server, listener net.Listener, ctx context.Context, server *waf.Server, listener net.Listener,
files *state.Files, ruleFiles *rules.Files, alertQueue *alerts.Queue, files *state.Files, ruleFiles *rules.Files, alertQueue *alerts.Queue,
@@ -395,7 +393,7 @@ func serve(
err = files.WriteAll() err = files.WriteAll()
if err != nil { if err != nil {
processLog.Error("writing the state and cache files failed", "error", err.Error()) processLog.Error("writing the state files failed", "error", err.Error())
return 1 return 1
} }
+18 -53
View File
@@ -38,7 +38,6 @@ const (
stateDir = "SWW_STATE_DIR" stateDir = "SWW_STATE_DIR"
stateWriteDelay = "SWW_STATE_WRITE_DELAY" stateWriteDelay = "SWW_STATE_WRITE_DELAY"
stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL" stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL"
cacheDir = "SWW_CACHE_DIR"
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY" rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS" rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
rulesDir = "SWW_RULES_DIR" rulesDir = "SWW_RULES_DIR"
@@ -161,12 +160,6 @@ func run(ctx context.Context, env map[string]string, out *output) int {
return "off", true return "off", true
} }
// The cache files go beside the state files unless the test
// says otherwise.
if !ok && name == cacheDir {
return env[stateDir], env[stateDir] != ""
}
return value, ok return value, ok
}, },
Stdout: out, Stdout: out,
@@ -706,7 +699,7 @@ func TestLookupDatabaseReplacedWhileRunningTakesEffect(t *testing.T) {
} }
} }
func TestBlocklistTriesAndCopiesKeptInListsJSONAcrossRestarts(t *testing.T) { func TestBlocklistTriesAndCopiesKeptInReputationJSONAcrossRestarts(t *testing.T) {
t.Parallel() t.Parallel()
const ( const (
@@ -739,11 +732,10 @@ func TestBlocklistTriesAndCopiesKeptInListsJSONAcrossRestarts(t *testing.T) {
t.Cleanup(server.Close) t.Cleanup(server.Close)
torURL, dropURL := server.URL+torPath, server.URL+dropPath torURL, dropURL := server.URL+torPath, server.URL+dropPath
cache := t.TempDir() dir := t.TempDir()
env := map[string]string{ env := map[string]string{
listenAddr: localhost + ":0", listenAddr: localhost + ":0",
stateDir: t.TempDir(), stateDir: dir,
cacheDir: cache,
rulesDir: t.TempDir(), rulesDir: t.TempDir(),
metricsToken: token, metricsToken: token,
instanceName: instance, instanceName: instance,
@@ -763,7 +755,7 @@ func TestBlocklistTriesAndCopiesKeptInListsJSONAcrossRestarts(t *testing.T) {
}) })
// Restarted with drop.txt named after it and the server answering, tor.txt // Restarted with drop.txt named after it and the server answering, tor.txt
// waits SWW_BLOCKLIST_REFRESH after its failed try, kept in lists.json, // waits SWW_BLOCKLIST_REFRESH after its failed try, kept in reputation.json,
// while drop.txt, never tried, is fetched at once. Lists are fetched in the // while drop.txt, never tried, is fetched at once. Lists are fetched in the
// order named, so once drop.txt refuses the client, tor.txt has had its turn. // order named, so once drop.txt refuses the client, tor.txt has had its turn.
failing.Store(false) failing.Store(false)
@@ -781,8 +773,8 @@ func TestBlocklistTriesAndCopiesKeptInListsJSONAcrossRestarts(t *testing.T) {
} }
// After another restart, with the server failing, the copy of drop.txt // After another restart, with the server failing, the copy of drop.txt
// kept in lists.json, its copyright line included, refuses the client // kept in reputation.json, its copyright line included, refuses the
// from the first request. // client from the first request.
failing.Store(true) failing.Store(true)
out = runUntilStopped(t, env, func(url string) { out = runUntilStopped(t, env, func(url string) {
@@ -790,16 +782,16 @@ func TestBlocklistTriesAndCopiesKeptInListsJSONAcrossRestarts(t *testing.T) {
}) })
wantDeniedByList(t, out.line(t, "type", "request"), dropURL) wantDeniedByList(t, out.line(t, "type", "request"), dropURL)
path := filepath.Join(cache, "lists.json") path := filepath.Join(dir, "reputation.json")
kept, err := os.ReadFile(path) //nolint:gosec // a file in the test's directory kept, err := os.ReadFile(path) //nolint:gosec // a file in the test's directory
if err != nil || !strings.Contains(string(kept), `"`+copyright+`"`) { if err != nil || !strings.Contains(string(kept), `"`+copyright+`"`) {
t.Errorf("lists.json holds\n%s\nwant the copy with %q (%v)", kept, copyright, t.Errorf("reputation.json holds\n%s\nwant the copy with %q (%v)", kept, copyright,
err) err)
} }
} }
func TestCrowdSecDecisionListKeptInListsJSONAcrossARestart(t *testing.T) { func TestCrowdSecDecisionListKeptInReputationJSONAcrossARestart(t *testing.T) {
t.Parallel() t.Parallel()
const bouncerKey = "crowdsec-key-0123456789abcdef" const bouncerKey = "crowdsec-key-0123456789abcdef"
@@ -822,11 +814,10 @@ func TestCrowdSecDecisionListKeptInListsJSONAcrossARestart(t *testing.T) {
})) }))
t.Cleanup(engine.Close) t.Cleanup(engine.Close)
dir, cache := t.TempDir(), t.TempDir() dir := t.TempDir()
env := map[string]string{ env := map[string]string{
listenAddr: localhost + ":0", listenAddr: localhost + ":0",
stateDir: dir, stateDir: dir,
cacheDir: cache,
rulesDir: t.TempDir(), rulesDir: t.TempDir(),
"SWW_CROWDSEC_LAPI_URL": engine.URL, "SWW_CROWDSEC_LAPI_URL": engine.URL,
"SWW_CROWDSEC_LAPI_KEY": bouncerKey, "SWW_CROWDSEC_LAPI_KEY": bouncerKey,
@@ -845,7 +836,7 @@ func TestCrowdSecDecisionListKeptInListsJSONAcrossARestart(t *testing.T) {
t.Errorf("bans.json holds %v, want the ban for crowdsec on %s", ban, placed) t.Errorf("bans.json holds %v, want the ban for crowdsec on %s", ban, placed)
} }
// Restarted with the engine down, the copy kept in lists.json bans // Restarted with the engine down, the copy kept in reputation.json bans
// another client in the netblock from its first request. // another client in the netblock from its first request.
down.Store(true) down.Store(true)
@@ -853,25 +844,22 @@ func TestCrowdSecDecisionListKeptInListsJSONAcrossARestart(t *testing.T) {
wantStatus(t, url, "203.0.113.10", http.StatusForbidden) wantStatus(t, url, "203.0.113.10", http.StatusForbidden)
}) })
// The key is in neither run's output, nor in a state or cache file. // The key is in neither run's output, nor in a state file.
files := []string{ files := []string{"bans.json", "reputation.json", "clients.json"}
filepath.Join(dir, "bans.json"), filepath.Join(dir, "clients.json"),
filepath.Join(cache, "lists.json"),
}
shown := make([]string, 0, len(files)+2) shown := make([]string, 0, len(files)+2)
shown = append(shown, first.text(), second.text()) shown = append(shown, first.text(), second.text())
for _, path := range files { for _, name := range files {
data, err := os.ReadFile(path) //nolint:gosec // the test's data, err := os.ReadFile(filepath.Join(dir, name)) //nolint:gosec // the test's
if err != nil { if err != nil {
t.Fatalf("read %s: %v", path, err) t.Fatalf("read %s: %v", name, err)
} }
shown = append(shown, string(data)) shown = append(shown, string(data))
} }
if all := strings.Join(shown, "\n"); strings.Contains(all, bouncerKey) { if all := strings.Join(shown, "\n"); strings.Contains(all, bouncerKey) {
t.Errorf("the key is shown in the output or the state and cache files:\n%s", all) t.Errorf("the key is shown in the output or the state files:\n%s", all)
} }
} }
@@ -1219,29 +1207,6 @@ func TestUnwritableStateDirStopsTheStart(t *testing.T) {
"SWW_STATE_DIR cannot be written: ") "SWW_STATE_DIR cannot be written: ")
} }
func TestUnwritableCacheDirStopsTheStart(t *testing.T) {
t.Parallel()
ctx, stop := context.WithTimeout(t.Context(), waitLimit)
defer stop()
out := &output{}
status := run(ctx, map[string]string{
listenAddr: localhost + ":0", stateDir: t.TempDir(), rulesDir: t.TempDir(),
cacheDir: filepath.Join(t.TempDir(), "missing"),
}, out)
if status != 1 {
t.Fatalf("exit status %d, want 1", status)
}
line := out.line(t, "msg", "cannot use the state and cache files")
if message, _ := line["error"].(string); !strings.HasPrefix(message,
"SWW_CACHE_DIR cannot be written: ") {
t.Errorf("start refused with %q, want SWW_CACHE_DIR named", message)
}
}
// wantStartRefused runs sww with its state files in dir, and // wantStartRefused runs sww with its state files in dir, and
// checks that it stops at start, with an error that starts with want. If // checks that it stops at start, with an error that starts with want. If
// it starts instead, it is stopped after waitLimit. // it starts instead, it is stopped after waitLimit.
@@ -1260,7 +1225,7 @@ func wantStartRefused(t *testing.T, dir, want string) {
t.Fatalf("exit status %d, want 1", status) t.Fatalf("exit status %d, want 1", status)
} }
line := out.line(t, "msg", "cannot use the state and cache files") line := out.line(t, "msg", "cannot use the state files")
message, _ := line["error"].(string) message, _ := line["error"].(string)
if !strings.HasPrefix(message, want) { if !strings.HasPrefix(message, want) {
+18 -41
View File
@@ -38,35 +38,6 @@ func TestMain(m *testing.M) {
os.Exit(sww.Main(testVersion)) os.Exit(sww.Main(testVersion))
} }
// startProcess starts cmd and returns a channel that gets its exit status. As
// the test ends, cmd is killed and the test waits for it to exit.
func startProcess(t *testing.T, cmd *exec.Cmd) <-chan int {
t.Helper()
err := cmd.Start()
if err != nil {
t.Fatalf("start: %v", err)
}
exited := make(chan int, 1)
waitReturned := make(chan struct{})
go func() {
_ = cmd.Wait()
exited <- cmd.ProcessState.ExitCode()
close(waitReturned)
}()
t.Cleanup(func() {
_ = cmd.Process.Kill()
<-waitReturned
})
return exited
}
func TestMainRunsWAF(t *testing.T) { func TestMainRunsWAF(t *testing.T) {
t.Parallel() t.Parallel()
@@ -83,9 +54,12 @@ func TestMainRunsWAF(t *testing.T) {
t.Fatalf("find the test binary: %v", err) t.Fatalf("find the test binary: %v", err)
} }
ctx, cancel := context.WithTimeout(t.Context(), 3*waitLimit)
defer cancel()
dir := t.TempDir() dir := t.TempDir()
out := &output{} out := &output{}
cmd := exec.CommandContext(t.Context(), binary) //nolint:gosec // this test binary cmd := exec.CommandContext(ctx, binary) //nolint:gosec // this test binary
cmd.Stdout = out cmd.Stdout = out
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
@@ -93,13 +67,20 @@ func TestMainRunsWAF(t *testing.T) {
commandForMain+"="+wafCommand, commandForMain+"="+wafCommand,
listenAddr+"="+localhost+":0", listenAddr+"="+localhost+":0",
stateDir+"="+dir, stateDir+"="+dir,
cacheDir+"="+dir,
rulesDir+"="+t.TempDir(), rulesDir+"="+t.TempDir(),
lookupSource+"=off") lookupSource+"=off")
exited := startProcess(t, cmd) err = cmd.Start()
if err != nil {
t.Fatalf("start: %v", err)
}
starting := out.lineBeforeExit(t, "starting", exited) t.Cleanup(func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
})
starting := out.line(t, "msg", "starting")
addr, _ := starting["address"].(string) addr, _ := starting["address"].(string)
ask(t, addr, "/", http.StatusOK, waf.DispositionAllow) ask(t, addr, "/", http.StatusOK, waf.DispositionAllow)
@@ -109,13 +90,9 @@ func TestMainRunsWAF(t *testing.T) {
t.Fatalf("signal: %v", err) t.Fatalf("signal: %v", err)
} }
select { err = cmd.Wait()
case status := <-exited: if err != nil {
if status != 0 { t.Fatalf("exit: %v; output:\n%s", err, out.text())
t.Fatalf("exit status %d, want 0; output:\n%s", status, out.text())
}
case <-time.After(waitLimit):
t.Fatal("still running after being told to stop")
} }
wantUTC(t, "the process line", starting["time"]) wantUTC(t, "the process line", starting["time"])
@@ -238,7 +215,7 @@ func runWAF(t *testing.T, env map[string]string, use func(addr string)) {
exited <- run(ctx, env, out) exited <- run(ctx, env, out)
}() }()
addr, _ := out.lineBeforeExit(t, "starting", exited)["address"].(string) addr, _ := out.line(t, "msg", "starting")["address"].(string)
use(addr) use(addr)
stop() stop()
+33 -108
View File
@@ -1,7 +1,6 @@
package waf_test package waf_test
import ( import (
"encoding/binary"
"fmt" "fmt"
"maps" "maps"
"net" "net"
@@ -12,13 +11,10 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"sync"
"testing" "testing"
"testing/synctest" "testing/synctest"
"time" "time"
"github.com/prometheus/client_golang/prometheus/testutil"
"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/lookup/lookuptest" "sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
@@ -34,8 +30,6 @@ const (
answerTimeLimit = 2 * time.Second answerTimeLimit = 2 * time.Second
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT. // defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
defaultLookupWait = 800 * time.Millisecond defaultLookupWait = 800 * time.Millisecond
// defaultReputationWait is the default of SWW_REPUTATION_TIMEOUT.
defaultReputationWait = 2 * time.Second
// The header limits SPEC.md gives. // The header limits SPEC.md gives.
cookieLimit = 50 cookieLimit = 50
headerFieldLimit = 100 headerFieldLimit = 100
@@ -634,9 +628,15 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
) { ) {
t.Parallel() t.Parallel()
// The lists' stand-in: one for GeoJS that answers nothing. held, askedGeoJS, _ := startHeldGeoJS(t)
held, _, _ := startHeldGeoJS(t)
resolver, releaseResolver := startHeldResolver(t) // A resolver that takes every query and answers none.
resolver, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", localhost+":0")
if err != nil {
t.Fatalf("listen: %v", err)
}
t.Cleanup(func() { _ = resolver.Close() })
server, out, _ := newWAFWith(t, func( server, out, _ := newWAFWith(t, func(
params waf.Params, params waf.Params,
@@ -644,117 +644,42 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
params.AbuseIPDBURL = silentAbuseIPDBURL params.AbuseIPDBURL = silentAbuseIPDBURL
return waf.New(params) return waf.New(params)
}, silentGeoJSURL, time.Now, map[string]string{ }, held, time.Now, map[string]string{
tagDeny: "country=KP", wafMode: block, tagDeny: "country=KP", wafMode: block,
dnsblZones: dnsblZone, dnsblResolver: resolver, dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(),
abuseIPDBKey: accountKey, abuseIPDBKey: accountKey,
blocklistURLs: held + "/drop.txt", asnLimitPercentURL: held + "/asn.txt", blocklistURLs: held + "/drop.txt", asnLimitPercentURL: held + "/asn.txt",
crowdSecURL: held, crowdSecKey: bouncerKey, crowdSecURL: held, crowdSecKey: bouncerKey,
}) })
// The lists are fetched outside the synctest bubble below: a wait on the // AbuseIPDB is asked only about a client with an offence.
// network inside it would keep its clock from moving on. group := netip.MustParsePrefix(client + "/32")
server.Limiter.AddToHistory(group, time.Now(), ratelimit.Request{Attack: true})
go server.Lists.Run(t.Context()) go server.Lists.Run(t.Context())
// In the bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays says, the status, took := askDirectly(t, server, func(req *http.Request) {
// answer takes exactly as long as waf waits, however busy the host is. req.Header.Set("User-Agent", "test")
// GeoJS and AbuseIPDB never answer there, without the network. The query to req.Header.Set("Accept", "*/*")
// the DNS blocklist waits on the network, which stops the bubble's clock, req.Host = siteA
// but it starts only after the wait for GeoJS.
synctest.Test(t, func(t *testing.T) {
// AbuseIPDB is asked only about a client with an offence.
group := netip.MustParsePrefix(client + "/32")
server.Limiter.AddToHistory(group, time.Now(), ratelimit.Request{Attack: true})
status, took := askDirectly(t, server, func(req *http.Request) {
req.Header.Set("User-Agent", "test")
req.Header.Set("Accept", "*/*")
req.Host = siteA
})
// The wait for GeoJS decides when the answer comes.
if status != http.StatusOK || took != defaultLookupWait || took >= answerTimeLimit {
t.Errorf("answered %d after %s, want %d after %s, within %s; output:\n%s",
status, took, http.StatusOK, defaultLookupWait, answerTimeLimit, out.text())
}
// Each outside source was asked, and none has answered. GeoJS is asked
// only about the clients waiting for it, here client alone.
if requests := testutil.ToFloat64(server.Metrics.GeoJSRequests); requests != 1 {
t.Errorf("%v requests to GeoJS, want 1", requests)
}
// The resolver holds the query until it is released below, so a waf
// that waited for the query would answer only once it had failed.
queries, failures := server.DNSBL.Queries(dnsblZone), server.DNSBL.Failures(dnsblZone)
if queries != 1 || failures != 0 {
t.Errorf("%d queries to the DNS blocklist, %d failed, want 1, none failed",
queries, failures)
}
if checked := server.AbuseIPDB.Checked(); checked != 1 {
t.Errorf("%d checks with AbuseIPDB, want 1", checked)
}
// The query is answered, and the check with AbuseIPDB abandoned,
// before the bubble ends.
releaseResolver()
time.Sleep(defaultReputationWait)
}) })
}
// How the stand-in for the resolver reads a query and makes it its reply. if status != http.StatusOK || took >= answerTimeLimit {
const ( t.Errorf("answered %d after %s, want %d within %s; output:\n%s",
// maxQueryLength is the longest query it reads. status, took, http.StatusOK, answerTimeLimit, out.text())
maxQueryLength = 512
// flagsAt is where a DNS message's flags start, after its id.
flagsAt = 2
// noSuchNameFlags mark a reply, to a query that asked for recursion,
// which is available, that no such name exists.
noSuchNameFlags = 0x8183
)
// startHeldResolver starts a stand-in for the resolver the DNSBL zones are
// asked through, and returns its address and release. Each query waits until
// release is called, and is then answered that no such name exists, so that
// the zone does not list the client.
func startHeldResolver(t *testing.T) (string, func()) {
t.Helper()
conn, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", localhost+":0")
if err != nil {
t.Fatalf("listen: %v", err)
} }
t.Cleanup(func() { _ = conn.Close() }) // Each outside source was asked, and none has answered. The stand-in for
// GeoJS is the lists' too, which name no address.
if !slices.Contains(askedGeoJS(), client) {
t.Errorf("GeoJS was asked about %q, not %s", askedGeoJS(), client)
}
var ( if queries := server.DNSBL.Queries(dnsblZone); queries != 1 {
released = make(chan struct{}) t.Errorf("%d queries to the DNS blocklist, want 1", queries)
once sync.Once }
)
go func() { if checked := server.AbuseIPDB.Checked(); checked != 1 {
query := make([]byte, maxQueryLength) t.Errorf("%d checks with AbuseIPDB, want 1", checked)
}
for {
n, from, err := conn.ReadFrom(query)
if err != nil {
return // conn is closed
}
<-released
// The query back, with the flags of the reply in place of its own.
reply := slices.Clone(query[:n])
binary.BigEndian.PutUint16(reply[flagsAt:], noSuchNameFlags)
_, _ = conn.WriteTo(reply, from)
}
}()
release := func() { once.Do(func() { close(released) }) }
// Run before conn is closed, so that no query is left waiting.
t.Cleanup(release)
return conn.LocalAddr().String(), release
} }
+2 -2
View File
@@ -16,7 +16,7 @@ import (
// The CrowdSec settings, and the tests' engine, which is never asked: each // The CrowdSec settings, and the tests' engine, which is never asked: each
// test puts in the copy of its decision list, at decisionsURL, that it // test puts in the copy of its decision list, at decisionsURL, that it
// needs, as lists.json would at start. // needs, as reputation.json would at start.
const ( const (
crowdSecURL = "SWW_CROWDSEC_LAPI_URL" crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY" crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
@@ -182,7 +182,7 @@ func TestObserveModeForwardsAClientTheCrowdSecDecisionListListsAndAlertsTheBan(
// loadDecisions puts into server's lists the copy of the decision list at // loadDecisions puts into server's lists the copy of the decision list at
// decisionsURL, answer, the engine's answer, fetched at fetched, as // decisionsURL, answer, the engine's answer, fetched at fetched, as
// lists.json would at start. // reputation.json would at start.
func loadDecisions( func loadDecisions(
t *testing.T, server *waf.Server, fetched time.Time, answer string, t *testing.T, server *waf.Server, fetched time.Time, answer string,
) { ) {
+2 -2
View File
@@ -86,8 +86,8 @@ type Params struct {
// Server is the server waf runs, with the parts whose state the state // Server is the server waf runs, with the parts whose state the state
// files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the // files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts, // lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
// AbuseIPDB's scores and checks spent, routewatch's answers, and the metrics. // AbuseIPDB's scores and checks spent, and the metrics. RouteWatch, nil while
// RouteWatch is nil while SWW_ROUTEWATCH_URL is unset. // SWW_ROUTEWATCH_URL is unset, keeps its answers in memory only.
type Server struct { type Server struct {
*http.Server *http.Server
+8 -8
View File
@@ -39,7 +39,7 @@ const (
) )
// The lists these tests name, which are never fetched: each test puts in // The lists these tests name, which are never fetched: each test puts in
// the copies it needs, as lists.json would at start. // the copies it needs, as reputation.json would at start.
const ( const (
dropURL = "https://lists.example/drop.txt" dropURL = "https://lists.example/drop.txt"
torURL = "https://lists.example/tor.txt" torURL = "https://lists.example/tor.txt"
@@ -374,7 +374,7 @@ const (
// The DNSBL zones these tests name, which are never asked about the // The DNSBL zones these tests name, which are never asked about the
// clients the tests send requests from: each test puts in the verdicts it // clients the tests send requests from: each test puts in the verdicts it
// needs, as dnsbl.json would at start. A query a test does start is // needs, as reputation.json would at start. A query a test does start is
// sent to noResolver, where nothing listens, so that none leaves the host. // sent to noResolver, where nothing listens, so that none leaves the host.
const ( const (
dnsblZone = "dnsbl.example" dnsblZone = "dnsbl.example"
@@ -759,7 +759,7 @@ func TestOnlyAClientThatHasCommittedAnOffenceIsCheckedWithAbuseIPDB(t *testing.T
clk.advance(time.Hour) clk.advance(time.Hour)
wantReputation(t, s.get(fromDE, http.StatusOK, forward)) wantReputation(t, s.get(fromDE, http.StatusOK, forward))
wantAbuseIPDBChecks(t, server, 1) wantAbuseIPDBChecks(t, server, 1)
waitUntil(func() bool { return len(server.AbuseIPDB.Snapshot()) == 1 }) waitUntil(func() bool { return len(server.AbuseIPDB.Snapshot().Scores) == 1 })
wantReputation(t, s.get(fromDE, http.StatusOK, forward), abuseipdb) wantReputation(t, s.get(fromDE, http.StatusOK, forward), abuseipdb)
s.get(fromKP, http.StatusOK, forward) s.get(fromKP, http.StatusOK, forward)
@@ -1072,7 +1072,7 @@ func verdictsFetched() time.Time {
// loadVerdicts puts into server's DNSBL, for each client listedBy names, // loadVerdicts puts into server's DNSBL, for each client listedBy names,
// a verdict of each zone SWW_DNSBL_ZONES names, fetched at // a verdict of each zone SWW_DNSBL_ZONES names, fetched at
// verdictsFetched, as dnsbl.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 *waf.Server, listedBy map[string][]string) { func loadVerdicts(server *waf.Server, listedBy map[string][]string) {
@@ -1091,8 +1091,8 @@ func loadVerdicts(server *waf.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 // client, an IPv4 address, fetched at verdictsFetched, as reputation.json
// abuseipdb-scores.json would at start. // would at start.
func loadScores(server *waf.Server, scores map[string]int64) { func loadScores(server *waf.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 {
@@ -1102,7 +1102,7 @@ func loadScores(server *waf.Server, scores map[string]int64) {
}) })
} }
server.AbuseIPDB.Load(kept) server.AbuseIPDB.Load(reputation.Checks{Scores: kept})
} }
// wantAbuseIPDBChecks checks how many clients server has checked with // wantAbuseIPDBChecks checks how many clients server has checked with
@@ -1116,7 +1116,7 @@ func wantAbuseIPDBChecks(t *testing.T, server *waf.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 lists.json would at start. // its lines, fetched at listsFetched, as reputation.json would at start.
func loadLists(t *testing.T, server *waf.Server, copies map[string][]string) { func loadLists(t *testing.T, server *waf.Server, copies map[string][]string) {
t.Helper() t.Helper()
+5 -9
View File
@@ -9,7 +9,6 @@
# - that a probe for /.env bans another client, which its next request bans # - that a probe for /.env bans another client, which its next request bans
# for good; # for good;
# - that only waf's container changes the state directory's owner; # - that only waf's container changes the state directory's owner;
# - that waf's container makes the cache directory for the sww user;
# - that the app still answers through rproxy with waf stopped, and # - that the app still answers through rproxy with waf stopped, and
# that once waf has started again, bans apply again; # that once waf has started again, bans apply again;
# - that `sv stop` stops rproxy in order, and `docker stop` stops each # - that `sv stop` stops rproxy in order, and `docker stop` stops each
@@ -154,9 +153,9 @@ refused_from() {
[ "$(status_from "$1" "$2")" = 403 ] [ "$(status_from "$1" "$2")" = 403 ]
} }
# owner_of <container> <dir>: print who owns dir there. # owner_of_state_dir <container>: print who owns /var/lib/sww there.
owner_of() { owner_of_state_dir() {
docker exec "$1" stat --format '%U' "$2" docker exec "$1" stat --format '%U' /var/lib/sww
} }
main() { main() {
@@ -174,14 +173,11 @@ main() {
start_app start_app
echo "example-app: the health checks pass" echo "example-app: the health checks pass"
[ "$(owner_of "$WAF" /var/lib/sww)" = sww ] || [ "$(owner_of_state_dir "$WAF")" = sww ] ||
fail "waf's container did not give the state directory to sww" fail "waf's container did not give the state directory to sww"
[ "$(owner_of "$APP" /var/lib/sww)" = root ] || [ "$(owner_of_state_dir "$APP")" = root ] ||
fail "the app's container changed the state directory's owner" fail "the app's container changed the state directory's owner"
echo "example-app: only waf's container changes the state directory's owner" echo "example-app: only waf's container changes the state directory's owner"
[ "$(owner_of "$WAF" /var/cache/sww)" = sww ] ||
fail "waf's container did not make the cache directory for sww"
echo "example-app: waf's container makes the cache directory for sww"
page="$(curl --fail --silent --show-error --max-time 10 "http://$address/")" || page="$(curl --fail --silent --show-error --max-time 10 "http://$address/")" ||
fail "no answer on port 8080" fail "no answer on port 8080"
+2 -8
View File
@@ -6,9 +6,8 @@
# waf listens on port 8090 unless SWW_LISTEN_ADDR says otherwise, and # waf listens on port 8090 unless SWW_LISTEN_ADDR says otherwise, and
# rproxy asks it there unless SWRP_WAF_BACKEND says otherwise. Unless # rproxy asks it there unless SWRP_WAF_BACKEND says otherwise. Unless
# SWW_STATE_DIR is set, the state files go in bin/state, beside the binary, # SWW_STATE_DIR is set, the state files go in bin/state, beside the binary,
# and unless SWW_CACHE_DIR is set, the cache files go in bin/cache. Unless # and unless SWW_RULES_DIR is set, the rule files are those of share/rules.d,
# SWW_RULES_DIR is set, the rule files are those of share/rules.d, which the # which the image ships.
# image ships.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -32,11 +31,6 @@ main() {
export SWW_STATE_DIR export SWW_STATE_DIR
mkdir -p "$SWW_STATE_DIR" mkdir -p "$SWW_STATE_DIR"
fi fi
if [ -z "${SWW_CACHE_DIR+set}" ]; then
SWW_CACHE_DIR="$ROOT/bin/cache"
export SWW_CACHE_DIR
mkdir -p "$SWW_CACHE_DIR"
fi
if [ -z "${SWW_RULES_DIR+set}" ]; then if [ -z "${SWW_RULES_DIR+set}" ]; then
SWW_RULES_DIR="$ROOT/share/rules.d" SWW_RULES_DIR="$ROOT/share/rules.d"
export SWW_RULES_DIR export SWW_RULES_DIR
+10 -28
View File
@@ -11,10 +11,7 @@ main() {
local command="${SWW_COMMAND:-rproxy}" local command="${SWW_COMMAND:-rproxy}"
case "$command" in case "$command" in
rproxy) ;; rproxy) ;;
waf) waf) give_state_dir_to_sww ;;
give_state_dir_to_sww
make_cache_dir_for_sww
;;
*) *)
printf 'SWW_COMMAND: "%s" is not rproxy or waf\n' \ printf 'SWW_COMMAND: "%s" is not rproxy or waf\n' \
"$command" >&2 "$command" >&2
@@ -34,34 +31,19 @@ main() {
# reports a directory it cannot write. # reports a directory it cannot write.
give_state_dir_to_sww() { give_state_dir_to_sww() {
local dir local dir
if dir="$(setting SWW_STATE_DIR /var/lib/sww)" && if dir="$(state_dir)" && [[ "$dir" == /* && -d "$dir" ]]; then
[[ "$dir" == /* && -d "$dir" ]]; then
chown -R sww:sww "$dir" || true chown -R sww:sww "$dir" || true
fi fi
} }
# make_cache_dir_for_sww makes the cache directory, SWW_CACHE_DIR in # state_dir prints SWW_STATE_DIR, /var/lib/sww when it is unset,
# either form, and gives it and every file in it to the sww user, as # or what is in the file SWW_STATE_DIR_FILE names, less every newline at
# give_state_dir_to_sww does the state directory. Unlike that one, it is # its end. It fails when both are set or the file cannot be read.
# made when it is missing, since the cache files need no volume. As there, state_dir() {
# a setting that cannot be read or is not an absolute path changes if [[ ! -v SWW_STATE_DIR_FILE ]]; then
# nothing, and a failure does not end the script. printf '%s\n' "${SWW_STATE_DIR-/var/lib/sww}"
make_cache_dir_for_sww() { elif [[ ! -v SWW_STATE_DIR ]]; then
local dir cat -- "$SWW_STATE_DIR_FILE" 2>/dev/null
if dir="$(setting SWW_CACHE_DIR /var/cache/sww)" && [[ "$dir" == /* ]]; then
{ mkdir -p -- "$dir" && chown -R sww:sww "$dir"; } || true
fi
}
# setting prints the setting named $1, $2 when it is unset, or what is in
# the file the setting $1_FILE names, less every newline at its end. It
# fails when both are set or the file cannot be read.
setting() {
local name="$1" file_name="$1_FILE"
if [[ ! -v "$file_name" ]]; then
printf '%s\n' "${!name-$2}"
elif [[ ! -v "$name" ]]; then
cat -- "${!file_name}" 2>/dev/null
else else
return 1 return 1
fi fi