Compare commits

..

59 Commits

Author SHA1 Message Date
Xe Iaso
6b09ac9543 chore: spelling
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-09 10:14:10 -04:00
Xe Iaso
de602116d0 fix(thr1): update spec to respond to feedback and evaluation against a private dataset
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-09 10:12:34 -04:00
Xe Iaso
3a4b1086af docs: add THR1 spec
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-04 23:14:17 -04:00
Markus Sommer
76fa3e01a5 docs(known-instances): add Alliance of Hessian Libraries (#611)
Signed-off-by: Markus Sommer <markus@splork.de>
2025-06-04 02:03:57 +00:00
Xe Iaso
f2db43ad4b feat: implement challenge registry (#607)
* feat: implement challenge method registry

This paves the way for implementing a no-js check method (#95) by making
the challenge providers more generic.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(lib/challenge): rename proof-of-work package to proofofwork

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(lib): make validated challenges a CounterVec

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(lib): annotate jwts with challenge method

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test(lib/challenge/proofofwork): implement tests

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test(lib): add smoke tests for known good and known bad config files

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(lib): use challenge.Impl#Issue when issuing challenges

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-04 02:01:58 +00:00
Xe Iaso
ba4412c907 chore(sponsors): add Raptor Computing Systems
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-03 17:49:28 -04:00
Xe Iaso
f184cd81e7 docs(faq): anubis does not mine bitcoin (#609)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-03 07:14:41 -04:00
Xe Iaso
59bfced8bf docs(admin/environments): update suggested HTTP headers
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-03 06:57:37 -04:00
Xe Iaso
780a935cb8 chore(sponsors): add wildbase
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-03 06:18:40 -04:00
Xe Iaso
f4bc1df797 chore(sponsors): add Uberspace
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-02 09:42:13 -04:00
dependabot[bot]
b496c90e86 build(deps): bump github.com/a-h/templ in the gomod group (#601)
Bumps the gomod group with 1 update: [github.com/a-h/templ](https://github.com/a-h/templ).


Updates `github.com/a-h/templ` from 0.3.865 to 0.3.887
- [Release notes](https://github.com/a-h/templ/releases)
- [Changelog](https://github.com/a-h/templ/blob/main/.goreleaser.yaml)
- [Commits](https://github.com/a-h/templ/compare/v0.3.865...v0.3.887)

---
updated-dependencies:
- dependency-name: github.com/a-h/templ
  dependency-version: 0.3.887
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 23:39:42 -04:00
dependabot[bot]
ec73bcbaf1 build(deps): bump docker/build-push-action in the github-actions group (#602)
Bumps the github-actions group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `docker/build-push-action` from 6.17.0 to 6.18.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](1dc7386353...263435318d)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 23:39:05 -04:00
dependabot[bot]
8d19eed200 build(deps-dev): bump esbuild from 0.25.4 to 0.25.5 in the npm group (#600)
Bumps the npm group with 1 update: [esbuild](https://github.com/evanw/esbuild).


Updates `esbuild` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 23:38:45 -04:00
Xe Iaso
ec733e93a5 v1.19.1
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-01 17:17:24 -04:00
Xe Iaso
51c384eefd fix(data/bots): bring back ai-robots-txt.yaml
Closes #599

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-01 17:15:00 -04:00
Xe Iaso
44d5ec0b6e chore: release version v1.19.0
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-01 16:35:03 -04:00
Xe Iaso
3bc9040a96 chore: bump yeet to v0.6.0
Gives us many nice things like:

* Windows support for yeet (modulo TecharoHQ/yeet#29)
* Removes the dependency on /bin/sh or /bin/bash thanks to
  mvdan.cc/sh/v3
* Checksum-compliant reproducible builds by default

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-06-01 16:33:08 -04:00
Corry Haines
de7dbfe6d6 Split up AI filtering files (#592)
* Split up AI filtering files

Create aggressive/moderate/permissive policies to allow administrators to choose their AI/LLM stance.

Aggressive policy matches existing default in Anubis.

Removes `Google-Extended` flag from `ai-robots-txt.yaml` as it doesn't exist in requests.

Rename `ai-robots-txt.yaml` to `ai-catchall.yaml` as the file is no longer a copy of the source repo/file.

* chore: spelling

* chore: fix embeds

* chore: fix data includes

* chore: fix file name typo

* chore: Ignore READMEs in configs

* chore(lib/policy/config): go tool goimports -w

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-06-01 20:21:18 +00:00
minihoot
77e0bbbce9 add my site to known-instances.md (#595)
Signed-off-by: minihoot <70825884+minihoot@users.noreply.github.com>
2025-06-01 15:28:16 +00:00
Zohiu
b4b5d2f82e docs(known-instances): add catgirl.click (#597)
Signed-off-by: Zohiu <102434603+Zohiu@users.noreply.github.com>
2025-06-01 14:55:02 +00:00
Aleksei
988fff77f1 docs(known-instances): add openwrt.org (#594)
Signed-off-by: Aleksei <Aloki@users.noreply.github.com>
2025-06-01 13:13:52 +00:00
Corry Haines
0d9ebebff6 Opt-in policies for OpenAI and MistralAI bots (#590)
* Define OpenAI bot ALLOW policies

Allows OpenAI bots to be allowlisted at the choice of the Anubis administrator. None are enabled by default.

* Define MistralAI bot ALLOW policy

* chore: spelling
2025-05-31 16:48:57 -04:00
Jason
ba00cdacd2 docs(known-instances): Add Gitea to the known instances list (#591)
Signed-off-by: Jason <57271945+jesentz@users.noreply.github.com>
2025-05-31 14:20:39 -04:00
Corry Haines
68a71c6a99 Add Applebot definition (#589)
* Add Applebot definition

Adds Apple's search indexing bot, and allowlists it by default.

Allowlisted by default because it is equivalent to Googlebot/Bingbot. Remove Applebot from `ai-robots-txt.yaml` for the same reasons.

Remove `Applebot-Extended` from `ai-robots-txt.yaml` as it has no effect.

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-05-31 10:18:32 -04:00
Xe Iaso
fbbab5a035 feat(lib): annotate cookies with what rule was passed (#576)
* feat(lib): annotate cookies with what rule was passed

Anubis JWTs now contain a policyRule claim with the cryptographic hash
of the rule that it passed. This is intended to help with a future move
away from proof of work being the default.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test(lib): fix cookie storage logic

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-30 14:42:02 -04:00
Jason Cameron
28ab29389c style(bench): small cleanup (#546)
* fix(bench): await benchmark loop and adjust outline styles in templates

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

* refactor: remove unused showContinueBar function and clean up video error handling

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

* style: format code for consistency and readability using prettier

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

---------

Signed-off-by: Jason Cameron <git@jasoncameron.dev>
2025-05-30 17:57:56 +00:00
Xe Iaso
497005ce3e fix(lib): only use the first five characters of Accept-Language header values (#588)
For some reason, Google Chrome will randomly send a "full"
Accept-Language header, and other times it will send a "partial"
Accept-Language header. This makes the challenge construction
inconsistent.

This commit fixes this issue by only considering up to the first five
characters of the Accept-Language header when making a challenge string.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-30 13:15:03 -04:00
Xe Iaso
669eb4ba4b fix(web): show Anubis version number on challenge pages (#587)
Closes #565

The page already had the version number embedded into it, but that was
not printed to the page. This prints the version number set at compile
time to the page.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-30 16:23:27 +00:00
Kian Kasad
6c4e739b0b feat(lib): Add anubis_proxied_requests_total metric (#570)
Adds a new counter metric which counts the number of requests proxied to
the upstream target, labeled with the host name of the request.
2025-05-30 12:21:04 -04:00
Xe Iaso
c8635357dc feat(yeetfile): build GOARCH=ppc64le packages (#583)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-30 00:35:56 -04:00
Xe Iaso
0ed905fd4e fix(internal/test): skip integration tests if SKIP_INTEGRATION is set (#586)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-30 00:49:53 +00:00
Xe Iaso
cd8a7eb2e2 feat(data): add x-firefox-ai default challenge rule (#580)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-28 21:08:39 +00:00
Xe Iaso
22c47f40d1 feat(expressions): add randInt function to allow making rules nondeterministic (#578)
This seems counter-intuitive at first glance, but let me cook.

One of the problems with Anubis is that the rule matching is super
deterministic. This means that attackers can figure out what patterns
they are hitting and change things to bypass them.

The randInt function lets you have rulesets behave nondeterministically.
This is a very easy way to hang yourself, but can be great to
psychologically mess with scraper operators. Consider this rule:

```yaml
- name: deny-lightpanda-sometimes
  action: DENY
  expression:
    all:
      - userAgent.matches("LightPanda")
      - randInt(16) >= 4
```

It would match about 75% of the time.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-28 16:36:27 -04:00
Xe Iaso
669671bd46 fix(internal): register mime type for .mjs files (#577)
Closes #575

I'm gonna be totally honest, I'm not sure if this is needed. However,
measure twice, cut once.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-28 13:52:48 +00:00
dependabot[bot]
6c247cdec8 build(deps): bump k8s.io/apimachinery in the gomod group (#524)
Bumps the gomod group with 1 update: [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery).


Updates `k8s.io/apimachinery` from 0.33.0 to 0.33.1
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.33.0...v0.33.1)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.33.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 09:41:25 -04:00
Kian Kasad
eeae28f459 feat(cli): Add --version flag (#572) 2025-05-28 04:16:44 +00:00
jordigh
9ba10262e3 add Weblate to known-instances.md (#571)
Signed-off-by: jordigh <jordigh@octave.org>
2025-05-28 00:03:04 +00:00
dependabot[bot]
a28a3d155a build(deps): bump astral-sh/setup-uv in the github-actions group (#558)
Bumps the github-actions group with 1 update: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv).


Updates `astral-sh/setup-uv` from 6.0.1 to 6.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](6b9c6063ab...f0ec1fc3b3)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 11:12:31 -04:00
Anna
086f43e3ca Create Anubis OpenRC init.d script (#561)
Signed-off-by: Anna @CyberTailor <cyber@sysrq.in>
2025-05-27 01:58:59 +00:00
Xe Iaso
fa1f2355ea v1.19.0-pre1
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-25 14:10:22 -04:00
Xe Iaso
0a56194825 docs(admin): add wordpress docs (#552)
Closes #551

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-24 17:00:37 -04:00
Jason Cameron
93e2447ba2 fix(expression): add validation for empty expression list in CEL (#545)
* fix(expression): add validation for empty ExpressionOrList

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

* fix(imports): block empty file imports with improved error checking logic

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

* docs(expression): improve validation to error on empty CEL expressions

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

---------

Signed-off-by: Jason Cameron <git@jasoncameron.dev>
2025-05-23 18:14:31 -04:00
Xe Iaso
51f875ff6f docs(native-install): vague gesturing at distribution package managers (#544)
Closes #530

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-23 16:51:45 +00:00
Xe Iaso
555a188dc3 fix(lib): record challenges issused over embedded HTML (#543)
Closes #531

This changes `anubis_challenges_issued` to be a vector counter that
records the challenge issuance method.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-23 12:45:41 -04:00
James Renken
6f08bcb481 feat: add TARGET_SNI to allow overriding the TLS handshake hostname when forwarding requests (#529)
* feat: add TARGET_SNI to allow overriding the TLS handshake hostname when forwarding requests

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-05-23 16:27:35 +00:00
Dryusdan
11081aac08 Bump AI-robots.txt rules to version 1.31 (#538)
* Bump AI-robots.txt rules to version 1.31

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-05-23 16:15:12 +00:00
Nathan Price
c78d830ecb docs/docs/admin/native-install.mdx: correct the path for the default configuration file installation (#535)
Using the native-install instructions, default.env was installed as /etc/anubis rather than /etc/anubis/default.env
2025-05-22 18:34:06 +00:00
Max Chernoff
5e7bfa5ec2 docs: REDIRECT_DOMAINS must include port numbers (#521)
For websites hosted on non-standard ports (`https://example:com:8443`,
`http://www.example.net:8080`, etc.), the domains listed in
`REDIRECT_DOMAINS` must contain the port number. This commit documents
this requirement on the Installation page.

Fixes #517.

Signed-off-by: Max Chernoff <git@maxchernoff.ca>
2025-05-19 00:38:46 +00:00
dependabot[bot]
7b8953303d build(deps): bump the github-actions group with 4 updates (#523)
Bumps the github-actions group with 4 updates: [docker/build-push-action](https://github.com/docker/build-push-action), [actions-hub/kubectl](https://github.com/actions-hub/kubectl), [check-spelling/check-spelling](https://github.com/check-spelling/check-spelling) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `docker/build-push-action` from 6.16.0 to 6.17.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](14487ce63c...1dc7386353)

Updates `actions-hub/kubectl` from 1.33.0 to 1.33.1
- [Release notes](https://github.com/actions-hub/kubectl/releases)
- [Commits](e81783053d...f632a31512)

Updates `check-spelling/check-spelling` from 0.0.24 to 0.0.25
- [Release notes](https://github.com/check-spelling/check-spelling/releases)
- [Commits](67debf5066...c635c2f3f7)

Updates `github/codeql-action` from 3.28.17 to 3.28.18
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](60168efe1c...ff0a06e83c)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions-hub/kubectl
  dependency-version: 1.33.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: check-spelling/check-spelling
  dependency-version: 0.0.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 3.28.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 00:35:59 +00:00
Xe Iaso
a6045d6698 fix(lib): properly clear out test cookie (#522)
Closes #520

For some reason, Chrome and Firefox are very picky over what they use to
match cookies that need to be deleted. Listen to me for my tale of woe:

The basic problem here is that cookies were an early hack added on the
side of the HTTP spec and they're basically impossible to upgrade or
change because who knows what relies on the exact behavior cookies use.
As a result, cookies don't just match by name, but by every setting that
exists on them. You can also have two cookies with the same name but
different values. This spec is a nightmare lol.

Even more fun: browsers will make up values for cookies if they aren't
set, meaning that getting a challenge token at `/docs` is semantically
different than a challenge token you got from `/`.

This PR fixes this issue by explicitly setting the "make sure cookie
support is working" cookie's path to `/`, meaning that it will always be
sent. Additionally, cookies are expired by setting the expiry time to
one minute in the past.

Hopefully this will fix it. I'm testing this locally and it seems to
work fine.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-18 22:41:26 +00:00
Lenni
e31e1ca5e7 Add reddit.nerdvpn.de to known instances (#518)
Signed-off-by: Lenni <87639068+Lenni-builder@users.noreply.github.com>
2025-05-18 20:27:36 +00:00
Xe Iaso
50e030d17e chore(docs/deploy): move to new cluster (#519)
* chore(docs/deploy): move to new cluster

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-18 20:25:12 +00:00
Xe Iaso
b640c567da feat(lib): ensure that clients store cookies (#501)
* feat(lib): ensure that clients store cookies

If a client is misconfigured and does not store cookies, then they can
get into a proof of work death spiral with Anubis. This fixes the
problem by setting a test cookie whenever the user gets hit with a
challenge page. If the test cookie is not there at challenge pass time,
then they are blocked. Administrators will also get a log message
explaining that the user intentionally broke cookie support and that this
behavior is not an Anubis bug.

Additionally, this ensures that clients being shown a challenge support
gzip-compressed responses by showing the challenge page at gzip level 1.
This level is intentionally chosen in order to minimize system impacts.

The ClearCookie function is made more generic to account for cookie
names as an argument. A correlating SetCookie function was also added to
make it easier to set cookies.

* chore(lib): clean up test code

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-16 13:03:40 -04:00
Dryusdan
9e9982ab5d feat(apps): Make SASL login work on bookstack with Anubis (#502)
* Make SASL login work on bookstack with Anubis

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-05-16 17:01:34 +00:00
Xe Iaso
3b98368aa9 feat(apps): add SearXNG instance tracker policy and Qualys Labs SSL testing rules (#512)
* feat(apps): add SearXNG instance tracker policy

* feat(apps): add Qualys SSL Labs policy

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: hyperdefined <contact@hyper.lol>
2025-05-16 16:59:15 +00:00
OatmealDome
76849531cd feat: add TARGET_HOST to allow overriding the Host header when forwarding requests (#507)
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-05-16 16:46:47 +00:00
Dryusdan
961320540b Bump AI-robots.txt rules to version 1.30 (#509)
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-05-16 16:40:25 +00:00
Xe Iaso
91c21fbb4b docs: add HTMX workaround (#511)
* docs: add HTMX workaround

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-16 16:37:39 +00:00
Xe Iaso
caf69be97b fix(systemd): add RuntimeDirectory (#510)
Closes #508

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-05-16 16:17:41 +00:00
108 changed files with 2399 additions and 966 deletions

View File

@@ -1,3 +1,4 @@
acs
aeacus aeacus
Aibrew Aibrew
alrest alrest
@@ -5,18 +6,21 @@ amazonbot
anthro anthro
anubis anubis
anubistest anubistest
Applebot
archlinux archlinux
badregexes badregexes
berr berr
bingbot bingbot
Bitcoin bitcoin
blogging blogging
Bluesky Bluesky
blueskybot blueskybot
boi boi
botnet botnet
BPort BPort
Brightbot
broked broked
Bytespider
cachebuster cachebuster
Caddyfile Caddyfile
caninetools caninetools
@@ -29,14 +33,17 @@ cgr
chainguard chainguard
chall chall
challengemozilla challengemozilla
checkpath
checkresult checkresult
chen chen
chibi chibi
cidranger cidranger
ckie ckie
cloudflare cloudflare
confd
containerbuild containerbuild
coreutils coreutils
Cotoyogi
CRDs CRDs
crt crt
daemonizing daemonizing
@@ -45,6 +52,7 @@ Debian
debrpm debrpm
decaymap decaymap
decompiling decompiling
Diffbot
discordapp discordapp
discordbot discordbot
distros distros
@@ -55,17 +63,24 @@ dracula
dronebl dronebl
droneblresponse droneblresponse
duckduckbot duckduckbot
eerror
ellenjoe ellenjoe
enbyware enbyware
enca
everyones everyones
evilbot evilbot
evilsite evilsite
expressionorlist expressionorlist
externalagent
externalfetcher
extldflags extldflags
facebookgo facebookgo
Factset
fastcgi fastcgi
fcf
fediverse fediverse
finfos finfos
Firecrawl
flagenv flagenv
Fordola Fordola
forgejo forgejo
@@ -80,19 +95,30 @@ goodbot
googlebot googlebot
govulncheck govulncheck
GPG GPG
GPT
gptbot
grw
Hashcash Hashcash
hashrate hashrate
headermap headermap
healthcheck healthcheck
hebis
hec hec
hmc hmc
hostable hostable
htmx
httpdebug httpdebug
hypertext
iaskspider
iat iat
ifm ifm
Imagesift
imgproxy
inp inp
iss iss
isset
ivh ivh
Jenomis
JGit JGit
journalctl journalctl
jshelter jshelter
@@ -104,23 +130,30 @@ keypair
KHTML KHTML
kinda kinda
KUBECONFIG KUBECONFIG
lcj
ldflags ldflags
letsencrypt letsencrypt
Lexentale
lgbt lgbt
licend licend
licstart licstart
lightpanda lightpanda
LIMSA
Linting Linting
linuxbrew linuxbrew
lkey
LLU LLU
loadbalancer loadbalancer
lol lol
LOMINSA
maintainership maintainership
malware malware
mcr mcr
memes memes
metrix
mimi mimi
minica minica
mistralai
Mojeek Mojeek
mojeekbot mojeekbot
mozilla mozilla
@@ -129,9 +162,15 @@ nginx
nobots nobots
NONINFRINGEMENT NONINFRINGEMENT
nosleep nosleep
OCOB
ogtags ogtags
omgili
omgilibot
onionservice onionservice
openai
openrc
pag pag
Pangu
parseable parseable
passthrough passthrough
Patreon Patreon
@@ -149,6 +188,7 @@ promauto
promhttp promhttp
pwcmd pwcmd
pwuser pwuser
qualys
qwant qwant
qwantbot qwantbot
rac rac
@@ -160,17 +200,27 @@ reputational
reqmeta reqmeta
risc risc
ruleset ruleset
runlevels
RUnlock RUnlock
sas sas
sasl
Scumm Scumm
searchbot
searx
sebest sebest
secretplans secretplans
selfsigned selfsigned
Semrush
setsebool setsebool
shellcheck
Sidetrade
sitemap sitemap
sls
sni
Sourceware Sourceware
Spambot Spambot
sparkline sparkline
spyderbot
srv srv
stackoverflow stackoverflow
startprecmd startprecmd
@@ -178,6 +228,7 @@ stoppostcmd
subgrid subgrid
subr subr
subrequest subrequest
SVCNAME
tagline tagline
tarballs tarballs
techaro techaro
@@ -185,12 +236,17 @@ techarohq
templ templ
templruntime templruntime
testarea testarea
thr
Tik
Timpibot
torproject torproject
traefik traefik
uberspace
unixhttpd unixhttpd
unmarshal unmarshal
uvx uvx
Varis Varis
Velen
vendored vendored
vhosts vhosts
videotest videotest
@@ -200,8 +256,12 @@ webmaster
webpage webpage
websecure websecure
websites websites
workaround Webzio
wildbase
wordpress
Workaround
workdir workdir
wpbot
xcaddy xcaddy
Xeact Xeact
xeiaso xeiaso
@@ -210,6 +270,7 @@ xesite
xess xess
xff xff
XForwarded XForwarded
XNG
XReal XReal
yae yae
YAMLTo YAMLTo

View File

@@ -20,9 +20,6 @@
# https://twitter.com/nyttypos/status/1898844061873639490 # https://twitter.com/nyttypos/status/1898844061873639490
#\([A-Z][a-z]{2,}(?: [a-z]+){3,}\)\.\s #\([A-Z][a-z]{2,}(?: [a-z]+){3,}\)\.\s
# Complete sentences shouldn't be in the middle of another sentence as a parenthetical.
(?<!\.)\.\),
# Complete sentences in parentheticals should not have a space before the period. # Complete sentences in parentheticals should not have a space before the period.
\s\.\)(?!.*\}\}) \s\.\)(?!.*\}\})

View File

@@ -128,3 +128,7 @@ go install(?:\s+[a-z]+\.[-@\w/.]+)+
# ignore long runs of a single character: # ignore long runs of a single character:
\b([A-Za-z])\g{-1}{3,}\b \b([A-Za-z])\g{-1}{3,}\b
# hit-count: 1 file-count: 1
# microsoft
\b(?:https?://|)(?:(?:(?:blogs|download\.visualstudio|docs|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%]*

View File

@@ -3,7 +3,7 @@ name: Docs deploy
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ "main" ] branches: ["main"]
permissions: permissions:
contents: read contents: read
@@ -24,7 +24,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Log into registry - name: Log into registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with: with:
registry: ghcr.io registry: ghcr.io
@@ -39,7 +39,7 @@ jobs:
- name: Build and push - name: Build and push
id: build id: build
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: ./docs context: ./docs
cache-to: type=gha cache-to: type=gha
@@ -50,15 +50,15 @@ jobs:
push: true push: true
- name: Apply k8s manifests to aeacus - name: Apply k8s manifests to aeacus
uses: actions-hub/kubectl@e81783053d902f50d752d21a6d99cf9689a652e1 # v1.33.0 uses: actions-hub/kubectl@f632a31512a74cb35940627c49c20f67723cbaaf # v1.33.1
env: env:
KUBE_CONFIG: ${{ secrets.AEACUS_KUBECONFIG }} KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with: with:
args: apply -k docs/manifest args: apply -k docs/manifest
- name: Apply k8s manifests to aeacus - name: Apply k8s manifests to aeacus
uses: actions-hub/kubectl@e81783053d902f50d752d21a6d99cf9689a652e1 # v1.33.0 uses: actions-hub/kubectl@f632a31512a74cb35940627c49c20f67723cbaaf # v1.33.1
env: env:
KUBE_CONFIG: ${{ secrets.AEACUS_KUBECONFIG }} KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with: with:
args: rollout restart -n default deploy/anubis-docs args: rollout restart -n default deploy/anubis-docs

View File

@@ -28,7 +28,7 @@ jobs:
- name: Build and push - name: Build and push
id: build id: build
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: ./docs context: ./docs
cache-to: type=gha cache-to: type=gha

View File

@@ -89,7 +89,7 @@ jobs:
steps: steps:
- name: check-spelling - name: check-spelling
id: spelling id: spelling
uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24 uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
with: with:
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }} suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
checkout: true checkout: true

View File

@@ -21,7 +21,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- name: Run zizmor 🌈 - name: Run zizmor 🌈
run: uvx zizmor --format sarif . > results.sarif run: uvx zizmor --format sarif . > results.sarif
@@ -29,7 +29,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -14,10 +14,32 @@
Anubis is brought to you by sponsors and donors like: Anubis is brought to you by sponsors and donors like:
[![Distrust](./docs/static/img/sponsors/distrust-logo.webp)](https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis) ### Diamond Tier
[![Terminal Trove](./docs/static/img/sponsors/terminal-trove.webp)](https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh)
[![canine.tools](./docs/static/img/sponsors/caninetools-logo.webp)](https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis) <a href="https://www.raptorcs.com/content/base/products.html">
[![Weblate](./docs/static/img/sponsors/weblate-logo.webp)](https://weblate.org/?utm_campaign=github&utm_medium=referral&utm_content=anubis) <img src="./docs/static/img/sponsors/raptor-computing-logo.webp" alt="Raptor Computing Systems" height=64 />
</a>
### Gold Tier
<a href="https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/distrust-logo.webp" alt="Distrust" height="64">
</a>
<a href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh">
<img src="./docs/static/img/sponsors/terminal-trove.webp" alt="Terminal Trove" height="64">
</a>
<a href="https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/caninetools-logo.webp" alt="canine.tools" height="64">
</a>
<a href="https://weblate.org/">
<img src="./docs/static/img/sponsors/weblate-logo.webp" alt="Weblate" height="64">
</a>
<a href="https://uberspace.de/">
<img src="./docs/static/img/sponsors/uberspace-logo.webp" alt="Uberspace" height="64">
</a>
<a href="https://wildbase.xyz/">
<img src="./docs/static/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64">
</a>
## Overview ## Overview

View File

@@ -1 +1 @@
1.18.0 1.19.1

View File

@@ -16,6 +16,8 @@ const CookieName = "techaro.lol-anubis-auth"
// WithDomainCookieName is the name that is prepended to the per-domain cookie used when COOKIE_DOMAIN is set. // WithDomainCookieName is the name that is prepended to the per-domain cookie used when COOKIE_DOMAIN is set.
const WithDomainCookieName = "techaro.lol-anubis-auth-for-" const WithDomainCookieName = "techaro.lol-anubis-auth-for-"
const TestCookieName = "techaro.lol-anubis-cookie-test-if-you-block-this-anubis-wont-work"
// CookieDefaultExpirationTime is the amount of time before the cookie/JWT expires. // CookieDefaultExpirationTime is the amount of time before the cookie/JWT expires.
const CookieDefaultExpirationTime = 7 * 24 * time.Hour const CookieDefaultExpirationTime = 7 * 24 * time.Hour

View File

@@ -56,6 +56,8 @@ var (
redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.") redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)") slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request") target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request")
targetSNI = flag.String("target-sni", "", "if set, the value of the TLS handshake hostname when forwarding requests to the target")
targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target")
targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend") targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend")
healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis") healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal") useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal")
@@ -65,7 +67,7 @@ var (
ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache") ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache")
extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder") extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals") webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
valkeyURL = flag.String("valkey-url", "", "Valkey URL for Anubis' state layer") versionFlag = flag.Bool("version", false, "print Anubis version")
) )
func keyFromHex(value string) (ed25519.PrivateKey, error) { func keyFromHex(value string) (ed25519.PrivateKey, error) {
@@ -136,7 +138,7 @@ func setupListener(network string, address string) (net.Listener, string) {
return listener, formattedAddress return listener, formattedAddress
} }
func makeReverseProxy(target string, insecureSkipVerify bool) (http.Handler, error) { func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool) (http.Handler, error) {
targetUri, err := url.Parse(target) targetUri, err := url.Parse(target)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse target URL: %w", err) return nil, fmt.Errorf("failed to parse target URL: %w", err)
@@ -158,16 +160,28 @@ func makeReverseProxy(target string, insecureSkipVerify bool) (http.Handler, err
transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport}) transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport})
} }
if insecureSkipVerify { if insecureSkipVerify || targetSNI != "" {
slog.Warn("TARGET_INSECURE_SKIP_VERIFY is set to true, TLS certificate validation will not be performed", "target", target) transport.TLSClientConfig = &tls.Config{}
transport.TLSClientConfig = &tls.Config{ if insecureSkipVerify {
InsecureSkipVerify: true, slog.Warn("TARGET_INSECURE_SKIP_VERIFY is set to true, TLS certificate validation will not be performed", "target", target)
transport.TLSClientConfig.InsecureSkipVerify = true
}
if targetSNI != "" {
transport.TLSClientConfig.ServerName = targetSNI
} }
} }
rp := httputil.NewSingleHostReverseProxy(targetUri) rp := httputil.NewSingleHostReverseProxy(targetUri)
rp.Transport = transport rp.Transport = transport
if targetHost != "" {
originalDirector := rp.Director
rp.Director = func(req *http.Request) {
originalDirector(req)
req.Host = targetHost
}
}
return rp, nil return rp, nil
} }
@@ -189,6 +203,11 @@ func main() {
flagenv.Parse() flagenv.Parse()
flag.Parse() flag.Parse()
if *versionFlag {
fmt.Println("Anubis", anubis.Version)
return
}
internal.InitSlog(*slogLevel) internal.InitSlog(*slogLevel)
if *extractResources != "" { if *extractResources != "" {
@@ -206,7 +225,7 @@ func main() {
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space // when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
if strings.TrimSpace(*target) != "" { if strings.TrimSpace(*target) != "" {
var err error var err error
rp, err = makeReverseProxy(*target, *targetInsecureSkipVerify) rp, err = makeReverseProxy(*target, *targetSNI, *targetHost, *targetInsecureSkipVerify)
if err != nil { if err != nil {
log.Fatalf("can't make reverse proxy: %v", err) log.Fatalf("can't make reverse proxy: %v", err)
} }

View File

@@ -0,0 +1,20 @@
# Make SASL login work on bookstack with Anubis
# https://www.bookstackapp.com/docs/admin/saml2-auth/
- name: allow-bookstack-sasl-login-routes
action: ALLOW
expression:
all:
- 'method == "POST"'
- path.startsWith("/saml2/acs")
- name: allow-bookstack-sasl-metadata-routes
action: ALLOW
expression:
all:
- 'method == "GET"'
- path.startsWith("/saml2/metadata")
- name: allow-bookstack-sasl-logout-routes
action: ALLOW
expression:
all:
- 'method == "GET"'
- path.startsWith("/saml2/sls")

View File

@@ -0,0 +1,7 @@
# This policy allows Qualys SSL Labs to fully work. (https://www.ssllabs.com/ssltest)
# IP ranges are taken from: https://qualys.my.site.com/discussions/s/article/000005823
- name: qualys-ssl-labs
action: ALLOW
remote_addresses:
- 64.41.200.0/24
- 2600:C02:1020:4202::/64

View File

@@ -0,0 +1,9 @@
# This policy allows SearXNG's instance tracker to work. (https://searx.space)
# IPs are taken from `check.searx.space` DNS records.
# https://toolbox.googleapps.com/apps/dig/#A/check.searx.space
# https://toolbox.googleapps.com/apps/dig/#AAAA/check.searx.space
- name: searx-checker
action: ALLOW
remote_addresses:
- 167.235.158.251/32
- 2a01:4f8:1c1c:8fc2::1/128

View File

@@ -4,7 +4,7 @@
"import": "(data)/bots/_deny-pathological.yaml" "import": "(data)/bots/_deny-pathological.yaml"
}, },
{ {
"import": "(data)/bots/ai-robots-txt.yaml" "import": "(data)/meta/ai-block-aggressive.yaml"
}, },
{ {
"import": "(data)/crawlers/_allow-good.yaml" "import": "(data)/crawlers/_allow-good.yaml"

View File

@@ -11,53 +11,51 @@
## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from. ## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
bots: bots:
# Pathological bots to deny # Pathological bots to deny
- # This correlates to data/bots/deny-pathological.yaml in the source tree - # This correlates to data/bots/deny-pathological.yaml in the source tree
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/deny-pathological.yaml # https://github.com/TecharoHQ/anubis/blob/main/data/bots/deny-pathological.yaml
import: (data)/bots/_deny-pathological.yaml import: (data)/bots/_deny-pathological.yaml
- import: (data)/bots/aggressive-brazilian-scrapers.yaml - import: (data)/bots/aggressive-brazilian-scrapers.yaml
# Enforce https://github.com/ai-robots-txt/ai.robots.txt # Aggressively block AI/LLM related bots/agents by default
- import: (data)/bots/ai-robots-txt.yaml - import: (data)/meta/ai-block-aggressive.yaml
# Search engine crawlers to allow, defaults to: # Consider replacing the aggressive AI policy with more selective policies:
# - Google (so they don't try to bypass Anubis) # - import: (data)/meta/ai-block-moderate.yaml
# - Bing # - import: (data)/meta/ai-block-permissive.yaml
# - DuckDuckGo
# - Qwant
# - The Internet Archive
# - Kagi
# - Marginalia
# - Mojeek
- import: (data)/crawlers/_allow-good.yaml
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt) # Search engine crawlers to allow, defaults to:
- import: (data)/common/keep-internet-working.yaml # - Google (so they don't try to bypass Anubis)
# - Apple
# - Bing
# - DuckDuckGo
# - Qwant
# - The Internet Archive
# - Kagi
# - Marginalia
# - Mojeek
- import: (data)/crawlers/_allow-good.yaml
# Challenge Firefox AI previews
- import: (data)/clients/x-firefox-ai.yaml
# # Punish any bot with "bot" in the user-agent string # Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
# # This is known to have a high false-positive rate, use at your own risk - import: (data)/common/keep-internet-working.yaml
# - name: generic-bot-catchall
# user_agent_regex: (?i:bot|crawler)
# action: CHALLENGE
# challenge:
# difficulty: 16 # impossible
# report_as: 4 # lie to the operator
# algorithm: slow # intentionally waste CPU cycles and time
# Generic catchall rule # # Punish any bot with "bot" in the user-agent string
- name: generic-browser # # This is known to have a high false-positive rate, use at your own risk
user_agent_regex: >- # - name: generic-bot-catchall
Mozilla|Opera # user_agent_regex: (?i:bot|crawler)
action: WEIGH # action: CHALLENGE
weight: # challenge:
adjust: 5 # difficulty: 16 # impossible
# report_as: 4 # lie to the operator
# algorithm: slow # intentionally waste CPU cycles and time
- name: high-pass-rate # Generic catchall rule
pass_rate: - name: generic-browser
rate: 0.8 user_agent_regex: >-
action: WEIGH Mozilla|Opera
weight: action: CHALLENGE
adjust: -15
dnsbl: false dnsbl: false
@@ -67,4 +65,4 @@ dnsbl: false
# will stop sending requests once they get it. # will stop sending requests once they get it.
status_codes: status_codes:
CHALLENGE: 200 CHALLENGE: 200
DENY: 200 DENY: 200

View File

@@ -1,5 +1,5 @@
- name: aggressive-brazilian-scrapers - name: deny-aggressive-brazilian-scrapers
action: WEIGH action: DENY
expression: expression:
any: any:
# Internet Explorer should be out of support # Internet Explorer should be out of support
@@ -18,9 +18,11 @@
- userAgent.contains("Win 9x") - userAgent.contains("Win 9x")
# Amazon does not have an Alexa Toolbar. # Amazon does not have an Alexa Toolbar.
- userAgent.contains("Alexa Toolbar") - userAgent.contains("Alexa Toolbar")
- name: challenge-aggressive-brazilian-scrapers
action: CHALLENGE
expression:
any:
# This is not released, even Windows 11 calls itself Windows 10 # This is not released, even Windows 11 calls itself Windows 10
- userAgent.contains("Windows NT 11.0") - userAgent.contains("Windows NT 11.0")
# iPods are not in common use # iPods are not in common use
- userAgent.contains("iPod") - userAgent.contains("iPod")
weight:
adjust: 10

View File

@@ -0,0 +1,11 @@
# Extensive list of AI-affiliated agents based on https://github.com/ai-robots-txt/ai.robots.txt
# Add new/undocumented agents here. Where documentation exists, consider moving to dedicated policy files.
# Notes on various agents:
# - Amazonbot: Well documented, but they refuse to state which agent collects training data.
# - anthropic-ai/Claude-Web: Undocumented by Anthropic. Possibly deprecated or hallucinations?
# - Perplexity*: Well documented, but they refuse to state which agent collects training data.
# Warning: May contain user agents that _must_ be blocked in robots.txt, or the opt-out will have no effect.
- name: "ai-catchall"
user_agent_regex: >-
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Brightbot 1.0|Bytespider|CCBot|Claude-Web|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|GoogleOther|GoogleOther-Image|GoogleOther-Video|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|NovaAct|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|QualifiedBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YouBot
action: DENY

View File

@@ -1,4 +1,6 @@
# Warning: Contains user agents that _must_ be blocked in robots.txt, or the opt-out will have no effect.
# Note: Blocks human-directed/non-training user agents
- name: "ai-robots-txt" - name: "ai-robots-txt"
user_agent_regex: >- user_agent_regex: >-
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Applebot|Applebot-Extended|Brightbot 1.0|Bytespider|CCBot|ChatGPT-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|NovaAct|OAI-SearchBot|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|YouBot AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Brightbot 1.0|Bytespider|CCBot|ChatGPT-User|Claude-SearchBot|Claude-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|MistralAI-User/1.0|NovaAct|OAI-SearchBot|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|QualifiedBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YouBot
action: DENY action: DENY

View File

@@ -1,6 +1,4 @@
- name: cloudflare-workers - name: cloudflare-workers
headers_regex: headers_regex:
CF-Worker: .* CF-Worker: .*
action: WEIGH action: DENY
weight:
adjust: 5

View File

@@ -1,14 +1,9 @@
- name: lightpanda - name: lightpanda
user_agent_regex: ^LightPanda/.*$ user_agent_regex: ^LightPanda/.*$
action: WEIGH action: DENY
weight:
adjust: 5
- name: headless-chrome - name: headless-chrome
user_agent_regex: HeadlessChrome user_agent_regex: HeadlessChrome
action: WEIGH action: DENY
weight:
adjust: 5
- name: headless-chromium - name: headless-chromium
user_agent_regex: HeadlessChromium user_agent_regex: HeadlessChromium
weight: action: DENY
adjust: 5

8
data/clients/ai.yaml Normal file
View File

@@ -0,0 +1,8 @@
# User agents that act on behalf of humans in AI tools, e.g. searching the web.
# Each entry should have a positive/ALLOW entry created as well, with further documentation.
# Exceptions:
# - Claude-User: No published IP allowlist
- name: "ai-clients"
user_agent_regex: >-
ChatGPT-User|Claude-User|MistralAI-User
action: DENY

View File

@@ -0,0 +1,10 @@
# Acts on behalf of user requests
# https://docs.mistral.ai/robots/
- name: mistral-mistralai-user
user_agent_regex: MistralAI-User/.+; \+https\://docs\.mistral\.ai/robots
action: ALLOW
# https://mistral.ai/mistralai-user-ips.json
remote_addresses: [
"20.240.160.161/32",
"20.240.160.1/32",
]

View File

@@ -0,0 +1,93 @@
# Acts on behalf of user requests
# https://platform.openai.com/docs/bots/overview-of-openai-crawlers
- name: openai-chatgpt-user
user_agent_regex: ChatGPT-User/.+; \+https\://openai\.com/bot
action: ALLOW
# https://openai.com/chatgpt-user.json
# curl 'https://openai.com/chatgpt-user.json' | jq '.prefixes.[].ipv4Prefix' | sed 's/$/,/'
remote_addresses: [
"13.65.138.112/28",
"23.98.179.16/28",
"13.65.138.96/28",
"172.183.222.128/28",
"20.102.212.144/28",
"40.116.73.208/28",
"172.183.143.224/28",
"52.190.190.16/28",
"13.83.237.176/28",
"51.8.155.64/28",
"74.249.86.176/28",
"51.8.155.48/28",
"20.55.229.144/28",
"135.237.131.208/28",
"135.237.133.48/28",
"51.8.155.112/28",
"135.237.133.112/28",
"52.159.249.96/28",
"52.190.137.16/28",
"52.255.111.112/28",
"40.84.181.32/28",
"172.178.141.112/28",
"52.190.142.64/28",
"172.178.140.144/28",
"52.190.137.144/28",
"172.178.141.128/28",
"57.154.187.32/28",
"4.196.118.112/28",
"20.193.50.32/28",
"20.215.188.192/28",
"20.215.214.16/28",
"4.197.22.112/28",
"4.197.115.112/28",
"172.213.21.16/28",
"172.213.11.144/28",
"172.213.12.112/28",
"172.213.21.144/28",
"20.90.7.144/28",
"57.154.175.0/28",
"57.154.174.112/28",
"52.236.94.144/28",
"137.135.191.176/28",
"23.98.186.192/28",
"23.98.186.96/28",
"23.98.186.176/28",
"23.98.186.64/28",
"68.221.67.192/28",
"68.221.67.160/28",
"13.83.167.128/28",
"20.228.106.176/28",
"52.159.227.32/28",
"68.220.57.64/28",
"172.213.21.112/28",
"68.221.67.224/28",
"68.221.75.16/28",
"20.97.189.96/28",
"52.252.113.240/28",
"52.230.163.32/28",
"172.212.159.64/28",
"52.255.111.80/28",
"52.255.111.0/28",
"4.151.241.240/28",
"52.255.111.32/28",
"52.255.111.48/28",
"52.255.111.16/28",
"52.230.164.176/28",
"52.176.139.176/28",
"52.173.234.16/28",
"4.151.71.176/28",
"4.151.119.48/28",
"52.255.109.112/28",
"52.255.109.80/28",
"20.161.75.208/28",
"68.154.28.96/28",
"52.255.109.128/28",
"52.225.75.208/28",
"52.190.139.48/28",
"68.221.67.240/28",
"52.156.77.144/28",
"52.148.129.32/28",
"40.84.221.208/28",
"104.210.139.224/28",
"40.84.221.224/28",
"104.210.139.192/28",
]

View File

@@ -0,0 +1,4 @@
# https://connect.mozilla.org/t5/firefox-labs/try-out-link-previews-in-firefox-labs-138-and-share-your/td-p/92012
- name: x-firefox-ai
action: CHALLENGE
expression: '"X-Firefox-Ai" in headers'

View File

@@ -1,5 +1,3 @@
- name: no-user-agent-string - name: no-user-agent-string
action: WEIGH action: DENY
expression: userAgent == "" expression: userAgent == ""
weight:
adjust: 10

View File

@@ -1,4 +1,5 @@
- import: (data)/crawlers/googlebot.yaml - import: (data)/crawlers/googlebot.yaml
- import: (data)/crawlers/applebot.yaml
- import: (data)/crawlers/bingbot.yaml - import: (data)/crawlers/bingbot.yaml
- import: (data)/crawlers/duckduckbot.yaml - import: (data)/crawlers/duckduckbot.yaml
- import: (data)/crawlers/qwantbot.yaml - import: (data)/crawlers/qwantbot.yaml

View File

@@ -0,0 +1,8 @@
# User agents that index exclusively for search in for AI systems.
# Each entry should have a positive/ALLOW entry created as well, with further documentation.
# Exceptions:
# - Claude-SearchBot: No published IP allowlist
- name: "ai-crawlers-search"
user_agent_regex: >-
OAI-SearchBot|Claude-SearchBot
action: DENY

View File

@@ -0,0 +1,8 @@
# User agents that crawl for training AI/LLM systems
# Each entry should have a positive/ALLOW entry created as well, with further documentation.
# Exceptions:
# - ClaudeBot: No published IP allowlist
- name: "ai-crawlers-training"
user_agent_regex: >-
GPTBot|ClaudeBot
action: DENY

View File

@@ -0,0 +1,20 @@
# Indexing for search and Siri
# https://support.apple.com/en-us/119829
- name: applebot
user_agent_regex: Applebot
action: ALLOW
# https://search.developer.apple.com/applebot.json
remote_addresses: [
"17.241.208.160/27",
"17.241.193.160/27",
"17.241.200.160/27",
"17.22.237.0/24",
"17.22.245.0/24",
"17.22.253.0/24",
"17.241.75.0/24",
"17.241.219.0/24",
"17.241.227.0/24",
"17.246.15.0/24",
"17.246.19.0/24",
"17.246.23.0/24",
]

View File

@@ -0,0 +1,16 @@
# Collects AI training data
# https://platform.openai.com/docs/bots/overview-of-openai-crawlers
- name: openai-gptbot
user_agent_regex: GPTBot/1\.1; \+https\://openai\.com/gptbot
action: ALLOW
# https://openai.com/gptbot.json
remote_addresses: [
"52.230.152.0/24",
"20.171.206.0/24",
"20.171.207.0/24",
"4.227.36.0/25",
"20.125.66.80/28",
"172.182.204.0/24",
"172.182.214.0/24",
"172.182.215.0/24",
]

View File

@@ -0,0 +1,13 @@
# Indexing for search, does not collect training data
# https://platform.openai.com/docs/bots/overview-of-openai-crawlers
- name: openai-searchbot
user_agent_regex: OAI-SearchBot/1\.0; \+https\://openai\.com/searchbot
action: ALLOW
# https://openai.com/searchbot.json
remote_addresses: [
"20.42.10.176/28",
"172.203.190.128/28",
"104.210.140.128/28",
"51.8.102.0/24",
"135.234.64.0/24"
]

View File

@@ -3,6 +3,6 @@ package data
import "embed" import "embed"
var ( var (
//go:embed botPolicies.yaml botPolicies.json all:apps all:bots all:clients all:common all:crawlers //go:embed botPolicies.yaml botPolicies.json all:apps all:bots all:clients all:common all:crawlers all:meta
BotPolicies embed.FS BotPolicies embed.FS
) )

5
data/meta/README.md Normal file
View File

@@ -0,0 +1,5 @@
# meta policies
Contains policies that exclusively reference policies in _multiple_ other data folders.
Akin to "stances" that the administrator can take, with reference to various topics, such as AI/LLM systems.

View File

@@ -0,0 +1,6 @@
# Blocks all AI/LLM associated user agents, regardless of purpose or human agency
# Warning: To completely block some AI/LLM training, such as with Google, you _must_ place flags in robots.txt.
- import: (data)/bots/ai-catchall.yaml
- import: (data)/clients/ai.yaml
- import: (data)/crawlers/ai-search.yaml
- import: (data)/crawlers/ai-training.yaml

View File

@@ -0,0 +1,7 @@
# Blocks all AI/LLM bots used for training or unknown/undocumented purposes.
# Permits user agents with explicitly documented non-training use, and published IP allowlists.
- import: (data)/bots/ai-catchall.yaml
- import: (data)/crawlers/ai-training.yaml
- import: (data)/crawlers/openai-searchbot.yaml
- import: (data)/clients/openai-chatgpt-user.yaml
- import: (data)/clients/mistral-mistralai-user.yaml

View File

@@ -0,0 +1,6 @@
# Permits all well documented AI/LLM user agents with published IP allowlists.
- import: (data)/bots/ai-catchall.yaml
- import: (data)/crawlers/openai-searchbot.yaml
- import: (data)/crawlers/openai-gptbot.yaml
- import: (data)/clients/openai-chatgpt-user.yaml
- import: (data)/clients/mistral-mistralai-user.yaml

View File

@@ -11,13 +11,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- Refactor challenge presentation logic to use a challenge registry
## v1.19.1: Jenomis cen Lexentale - Echo 1
Return `data/bots/ai-robots-txt.yaml` to avoid breaking configs [#599](https://github.com/TecharoHQ/anubis/issues/599)
## v1.19.0: Jenomis cen Lexentale
Mostly a bunch of small features, no big ticket things this time.
- Record if challenges were issued via the API or via embedded JSON in the challenge page HTML ([#531](https://github.com/TecharoHQ/anubis/issues/531))
- Ensure that clients that are shown a challenge support storing cookies
- Imprint the version number into challenge pages
- Encode challenge pages with gzip level 1
- Add PowerPC 64 bit little-endian builds (`GOARCH=ppc64le`)
- Add `check-spelling` for spell checking - Add `check-spelling` for spell checking
- Add `--target-insecure-skip-verify` flag/envvar to allow Anubis to hit a self-signed HTTPS backend - Add `--target-insecure-skip-verify` flag/envvar to allow Anubis to hit a self-signed HTTPS backend
- Minor adjustments to FreeBSD rc.d script to allow for more flexible configuration. - Minor adjustments to FreeBSD rc.d script to allow for more flexible configuration.
- Added Podman and Docker support for running Playwright tests - Added Podman and Docker support for running Playwright tests
- Add a default rule to throw challenges when a request with the `X-Firefox-Ai` header is set.
- Updated the nonce value in the challenge JWT cookie to be a string instead of a number - Updated the nonce value in the challenge JWT cookie to be a string instead of a number
- Rename cookies in response to user feedback - Rename cookies in response to user feedback
- Ensure cookie renaming is consistent across configuration options - Ensure cookie renaming is consistent across configuration options
- Add Bookstack app in data
- Truncate everything but the first five characters of Accept-Language headers when making challenges
- Ensure client JavaScript is served with Content-Type text/javascript.
- Add `--target-host` flag/envvar to allow changing the value of the Host header in requests forwarded to the target service.
- Bump AI-robots.txt to version 1.31
- Add `RuntimeDirectory` to systemd unit settings so native packages can listen over unix sockets
- Added SearXNG instance tracker whitelist policy
- Added Qualys SSL Labs whitelist policy
- Fixed cookie deletion logic ([#520](https://github.com/TecharoHQ/anubis/issues/520), [#522](https://github.com/TecharoHQ/anubis/pull/522))
- Add `--target-sni` flag/envvar to allow changing the value of the TLS handshake hostname in requests forwarded to the target service.
- Fixed CEL expression matching validator to now properly error out when it receives empty expressions
- Added OpenRC init.d script.
- Added `--version` flag.
- Added `anubis_proxied_requests_total` metric to count proxied requests.
- Add `Applebot` as "good" web crawler
- Reorganize AI/LLM crawler blocking into three separate stances, maintaining existing status quo as default.
- Split out AI/LLM user agent blocking policies, adding documentation for each.
## v1.18.0: Varis zos Galvus ## v1.18.0: Varis zos Galvus
@@ -43,7 +76,7 @@ Or as complicated as:
expression: expression:
all: all:
- >- - >-
( (
userAgent.startsWith("git/") || userAgent.startsWith("git/") ||
userAgent.contains("libgit") || userAgent.contains("libgit") ||
userAgent.startsWith("go-git") || userAgent.startsWith("go-git") ||

View File

@@ -143,7 +143,29 @@ Anubis would return a challenge because all of those conditions are true.
## Functions exposed to Anubis expressions ## Functions exposed to Anubis expressions
There are currently no functions from the Anubis runtime exposed to expressions. This will change in the future. Anubis expressions can be augmented with the following functions:
### `randInt`
```ts
function randInt(n: int): int;
```
randInt returns a randomly selected integer value in the range of `[0,n)`. This is a thin wrapper around [Go's math/rand#Intn](https://pkg.go.dev/math/rand#Intn). Be careful with this as it may cause inconsistent behavior for genuine users.
This is best applied when doing explicit block rules, eg:
```yaml
# Denies LightPanda about 75% of the time on average
- name: deny-lightpanda-sometimes
action: DENY
expression:
all:
- userAgent.matches("LightPanda")
- randInt(16) >= 4
```
It seems counter-intuitive to allow known bad clients through sometimes, but this allows you to confuse attackers by making Anubis' behavior random. Adjust the thresholds and numbers as facts and circumstances demand.
## Life advice ## Life advice

View File

@@ -14,7 +14,7 @@ EG:
{ {
"bots": [ "bots": [
{ {
"import": "(data)/bots/ai-robots-txt.yaml" "import": "(data)/bots/ai-catchall.yaml"
}, },
{ {
"import": "(data)/bots/cloudflare-workers.yaml" "import": "(data)/bots/cloudflare-workers.yaml"
@@ -29,8 +29,8 @@ EG:
```yaml ```yaml
bots: bots:
# Pathological bots to deny # Pathological bots to deny
- # This correlates to data/bots/ai-robots-txt.yaml in the source tree - # This correlates to data/bots/ai-catchall.yaml in the source tree
import: (data)/bots/ai-robots-txt.yaml import: (data)/bots/ai-catchall.yaml
- import: (data)/bots/cloudflare-workers.yaml - import: (data)/bots/cloudflare-workers.yaml
``` ```
@@ -46,7 +46,7 @@ Of note, a bot rule can either have inline bot configuration or import a bot con
{ {
"bots": [ "bots": [
{ {
"import": "(data)/bots/ai-robots-txt.yaml", "import": "(data)/bots/ai-catchall.yaml",
"name": "generic-browser", "name": "generic-browser",
"user_agent_regex": "Mozilla|Opera\n", "user_agent_regex": "Mozilla|Opera\n",
"action": "CHALLENGE" "action": "CHALLENGE"
@@ -60,7 +60,7 @@ Of note, a bot rule can either have inline bot configuration or import a bot con
```yaml ```yaml
bots: bots:
- import: (data)/bots/ai-robots-txt.yaml - import: (data)/bots/ai-catchall.yaml
name: generic-browser name: generic-browser
user_agent_regex: > user_agent_regex: >
Mozilla|Opera Mozilla|Opera
@@ -167,7 +167,7 @@ static
├── botPolicies.json ├── botPolicies.json
├── botPolicies.yaml ├── botPolicies.yaml
├── bots ├── bots
│ ├── ai-robots-txt.yaml │ ├── ai-catchall.yaml
│ ├── cloudflare-workers.yaml │ ├── cloudflare-workers.yaml
│ ├── headless-browsers.yaml │ ├── headless-browsers.yaml
│ └── us-ai-scraper.yaml │ └── us-ai-scraper.yaml

View File

@@ -92,6 +92,7 @@ Assuming you are protecting `anubistest.techaro.lol`, you need the following ser
# throw an "admin misconfiguration" error. # throw an "admin misconfiguration" error.
RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR} RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR}
RequestHeader set X-Forwarded-Proto "https" RequestHeader set X-Forwarded-Proto "https"
RequestHeader set "X-Http-Version" "%{SERVER_PROTOCOL}s"
ProxyPreserveHost On ProxyPreserveHost On

View File

@@ -61,6 +61,7 @@ server {
location / { location / {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Http-Version $server_protocol;
proxy_pass http://anubis; proxy_pass http://anubis;
} }

View File

@@ -3,11 +3,10 @@ id: traefik
title: Traefik title: Traefik
--- ---
:::note :::note
This only talks about integration through Compose, This only talks about integration through Compose,
but it also applies to docker cli options. but it also applies to docker cli options.
::: :::

View File

@@ -0,0 +1,8 @@
{
"label": "Frameworks",
"position": 30,
"link": {
"type": "generated-index",
"description": "Information about getting specific frameworks or tools working with Anubis."
}
}

View File

@@ -0,0 +1,45 @@
# HTMX
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
[HTMX](https://htmx.org) is a framework that enables you to write applications using hypertext as the engine of application state. This enables you to simplify you server side code by having it return HTML instead of JSON. This can interfere with Anubis because Anubis challenge pages also return HTML.
To work around this, you can make a custom [expression](../configuration/expressions.mdx) rule that allows HTMX requests if the user has passed a challenge in the past:
<Tabs>
<TabItem value="json" label="JSON">
```json
{
"name": "allow-htmx-iff-already-passed-challenge",
"action": "ALLOW",
"expression": {
"all": [
"\"Cookie\" in headers",
"headers[\"Cookie\"].contains(\"anubis-auth\")",
"\"Hx-Request\" in headers",
"headers[\"Hx-Request\"] == \"true\""
]
}
}
```
</TabItem>
<TabItem value="yaml" label="YAML" default>
```yaml
- name: allow-htmx-iff-already-passed-challenge
action: ALLOW
expression:
all:
- '"Cookie" in headers'
- 'headers["Cookie"].contains("anubis-auth")'
- '"Hx-Request" in headers'
- 'headers["Hx-Request"] == "true"'
```
</TabItem>
</Tabs>
This will reduce some security because it does not assert the validity of the Anubis auth cookie, however in trade it improves the experience for existing users.

View File

@@ -0,0 +1,39 @@
# Wordpress
Wordpress is the most popular blog engine on the planet.
## Using a multi-site setup with Anubis
If you have a multi-site setup where traffic goes through Anubis like this:
```mermaid
---
title: Apache as tls terminator and HTTP router
---
flowchart LR
T(User Traffic)
subgraph Apache 2
TCP(TCP 80/443)
US(TCP 3001)
end
An(Anubis)
B(Backend)
T --> |TLS termination| TCP
TCP --> |Traffic filtering| An
An --> |Happy traffic| US
US --> |whatever you're doing| B
```
Wordpress may not realize that the underlying connection is being done over HTTPS. This could lead to a redirect loop in the `/wp-admin/` routes. In order to fix this, add the following to your `wp-config.php` file:
```php
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
$_SERVER['SERVER_PORT'] = 443;
}
```
This will make Wordpress think that your connection is over HTTPS instead of plain HTTP.

View File

@@ -54,7 +54,7 @@ Anubis uses these environment variables for configuration:
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints. For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. | | `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints. For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` | | `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. | | `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information. | | `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.<br/><br/>Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. |
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. | | `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. | | `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. | | `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
@@ -66,7 +66,7 @@ Anubis uses these environment variables for configuration:
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. | | `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. |
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. | | `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. |
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. | | `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain. | | `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.<br/><br/>Note that if you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here. |
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. | | `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
| `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. | | `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. |
| `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. | | `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. |
@@ -84,6 +84,8 @@ If you don't know or understand what these settings mean, ignore them. These are
| Environment Variable | Default value | Explanation | | Environment Variable | Default value | Explanation |
| :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. | | `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
</details> </details>

View File

@@ -49,7 +49,7 @@ sudo install -D ./run/anubis@.service /etc/systemd/system
Install the default configuration file to your system: Install the default configuration file to your system:
```text ```text
sudo install -D ./run/default.env /etc/anubis sudo install -D ./run/default.env /etc/anubis/default.env
``` ```
</TabItem> </TabItem>
@@ -77,6 +77,13 @@ Install Anubis with `rpm`:
sudo rpm -ivh ./anubis-$VERSION.$ARCH.rpm sudo rpm -ivh ./anubis-$VERSION.$ARCH.rpm
``` ```
</TabItem>
<TabItem value="distro" label="Package managers">
Some Linux distributions offer Anubis [as a native package](https://repology.org/project/anubis-anti-crawler/versions). If you want to install Anubis from your distribution's package manager, consult any upstream documentation for how to install the package. It will either be named `anubis`, `www-apps/anubis` or `www/anubis`.
If you use a systemd-flavoured distribution, then follow the setup instructions for Debian or Red Hat Linux.
</TabItem> </TabItem>
</Tabs> </Tabs>

View File

@@ -0,0 +1,202 @@
# Techaro HTTP Request Fingerprinting Version 1
The naïve way to identify HTTP clients is to use the HTTP User-Agent string as a signal. In an ideal world, this would give you a perfect view of what clients are connecting to your server. We do not live in that ideal world. As such, we need an alternative method that can scale to the world we have.
## Prior Art
The biggest source of prior art is [FoxIO's JA4H fingerprinting method](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4H.md). This is fine, but there's a problem with it in the real world: Go doesn't allow you to observe the order headers arrived in. As Anubis is written in Go and I don't feel like boiling the HTTP server ocean today, there needs to be an alternative.
## THR1
The fingerprint consists of four concatenated components:
```text
<thr1_head>_<thr1_lang>_<thr1_sec>_<thr1_ua>_<thr1_enc>
```
Example:
```text
get201004_enca-d6b272e5b_sec-a9649072c_2a347fcf7_zs
```
Each component is described below:
### `thr1_head`
Overall request summary of method, protocol, and header counts:
- First three letters of the HTTP method, lowercased (e.g. get, pos).
- HTTP protocol version formatted in two digits (`10` for HTTP/1.0, `11` for HTTP/1.1, `20` for HTTP/2, `30` for HTTP/3 etc.).
- If present, prefer the HTTP protocol version in `X-Http-Version`.
- Number of HTTP headers sent by the client, zero-padded to two digits (e.g. `10`).
- Number of `Sec-*` headers sent by the client, zero-padded to two digits (e.g. `04`).
Example:
```text
get201004
```
### `thr1_lang`
`Accept-Language` header details.
- If no `Accept-Language` header is set, then:
```
-000000000
```
- Otherwise:
- The first 4 alphanumeric characters of the header value (lowercased, right-padded with `0` to length 4), e.g. `enca`.
- The first 9 hex characters of the SHA-256 hash of the full `Accept-Language` header value.
Example:
```
enca-d6b272e5b
```
### `thr1_sec`
Details about the `Sec-*` headers sent by the client.
```
thr1_sec = "sec-" + HASH9
```
Where:
- Collect **all headers whose names start with `sec-` (case-insensitive)**, excluding `Sec-Fetch-User`.
- For each header:
1. Normalize the header name by lowercasing.
2. If the header is one of the `Sec-CH-UA` family:
- `Sec-CH-UA`
- `Sec-CH-UA-Mobile`
- `Sec-CH-UA-Platform`
- `Sec-CH-UA-Platform-Version`
- `Sec-CH-UA-Model`
- `Sec-CH-UA-Full-Version`
Apply **special normalization rules** (see below).
3. For all other `sec-` headers:
- Unquote values if quoted.
- Trim leading/trailing whitespace.
- Keep the value as-is (do not parse further).
- Sort all included headers by their normalized header name (ASCII order).
- Serialize each header as:
```text
<header_name>:<normalized_value>
```
- Join all serialized lines with `\n`.
- Compute SHA-256 hash of the resulting canonical string.
- Take the first 9 hex characters of the hash and prefix with `sec-`.
Example:
```text
sec-a9649072c
```
#### Special Normalization Rules for `Sec-CH-UA*` headers
| Header | Normalization |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Sec-CH-UA` | Parse into `{brand, version}` pairs. Omit any with brand `"Not=A?Brand"`. Sort by brand ASC. Serialize as: `ua:Brand1/Version1,Brand2/Version2,...` |
| `Sec-CH-UA-Mobile` | Convert `"?1"` → `true`, `"?0"` → `false`. Serialize as: `mobile:true` or `mobile:false` |
| `Sec-CH-UA-Platform` | Lowercase, unquoted, trimmed. Serialize as: `platform:<value>` |
| `Sec-CH-UA-Platform-Version` | Unquoted, trimmed. Serialize as: `platform_version:<value>` |
| `Sec-CH-UA-Model` | Unquoted, trimmed. Serialize as: `model:<value>` |
| `Sec-CH-UA-Full-Version` | Unquoted, trimmed. Serialize as: `full_version:<value>` |
Given these headers:
```text
Sec-CH-UA: "Google Chrome";v="123", "Not=A?Brand";v="8", "Chromium";v="123"
Sec-CH-UA-Mobile: ?1
Sec-CH-UA-Platform: "Windows"
Sec-CH-UA-Platform-Version: "10.0.0"
Sec-CH-UA-Model: "Pixel 7"
Sec-CH-UA-Full-Version: "123.0.6312.122"
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
```
Normalized canonical string before hashing:
```text
sec-fetch-dest:document
sec-fetch-mode:navigate
mobile:true
platform:windows
platform_version:10.0.0
full_version:123.0.6312.122
model:Pixel 7
ua:Chromium/123,Google Chrome/123
```
Then sort by header name:
```text
full_version:123.0.6312.122
mobile:true
model:Pixel 7
platform:windows
platform_version:10.0.0
sec-fetch-dest:document
sec-fetch-mode:navigate
ua:Chromium/123,Google Chrome/123
```
### `thr1_ua`
SHA256 fingerprint of the `User-Agent` string, taking the first 9 hex digits.
Example output:
```text
2a347fcf7
```
### `thr1_enc`
Heres the updated spec and Go implementation for the `thr1_enc` (compression) component, now including:
- **Most preferred compression encoding** (`*`, `gzip`, `deflate`, `br`, `zstd`)
- **Number of encodings declared**, truncated to **two digits** (`01``99`, capped)
---
### ✅ `thr1_enc` Spec (Revised)
**Format:**
```
<preferred_encoding>-<count>
```
- `preferred_encoding` is the first matching value in this priority order:
1. `*`
2. `gzip`
3. `deflate`
4. `br`
5. `zstd`
- If none match, use `none`
- `count` is the number of encoding options, zero-padded to 2 digits (max 99)
**Examples:**
- `gzip, deflate` → `gzip-02`
- `gzip;q=0.9, br;q=0.8` → `gzip-02`
- `zstd` → `zstd-01`
- `bogus` → `none-01`
- _empty_ → `none-00`

View File

@@ -19,10 +19,44 @@ title: Anubis
Anubis is brought to you by sponsors and donors like: Anubis is brought to you by sponsors and donors like:
[![Distrust](/img/sponsors/distrust-logo.webp)](https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis) ### Diamond Tier
[![Terminal Trove](/img/sponsors/terminal-trove.webp)](https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh)
[![canine.tools](/img/sponsors/caninetools-logo.webp)](https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis) <a href="https://www.raptorcs.com/content/base/products.html">
[![Weblate](/img/sponsors/weblate-logo.webp)](https://weblate.org/?utm_campaign=github&utm_medium=referral&utm_content=anubis) <img
src="/img/sponsors/raptor-computing-logo.webp"
alt="Raptor Computing Systems"
height="64"
/>
</a>
### Gold Tier
<a href="https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="/img/sponsors/distrust-logo.webp" alt="Distrust" height="64" />
</a>
<a href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh">
<img
src="/img/sponsors/terminal-trove.webp"
alt="Terminal Trove"
height="64"
/>
</a>
<a href="https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img
src="/img/sponsors/caninetools-logo.webp"
alt="canine.tools"
height="64"
/>
</a>
<a href="https://weblate.org/">
<img src="/img/sponsors/weblate-logo.webp" alt="Weblate" height="64" />
</a>
<a href="https://uberspace.de/">
<img src="/img/sponsors/uberspace-logo.webp" alt="Uberspace" height="64" />
</a>
<a href="https://wildbase.xyz/">
<img src="/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64" />
</a>
## Overview ## Overview

View File

@@ -18,3 +18,11 @@ Anubis uses [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_W
2. Web Workers allow you to do multithreaded execution of JavaScript code. This lets Anubis run its checks in parallel across all your system cores so that the challenge can complete as fast as possible. In the last decade, most CPU advancements have come from making cores and code extremely parallel. Using Web Workers lets Anubis take advantage of your hardware as much as possible so that the challenge finishes as fast as possible. 2. Web Workers allow you to do multithreaded execution of JavaScript code. This lets Anubis run its checks in parallel across all your system cores so that the challenge can complete as fast as possible. In the last decade, most CPU advancements have come from making cores and code extremely parallel. Using Web Workers lets Anubis take advantage of your hardware as much as possible so that the challenge finishes as fast as possible.
If you use a browser extension such as [JShelter](https://jshelter.org/), you will need to [modify your JShelter configuration](./known-broken-extensions.md#jshelter) to allow Anubis' proof of work computation to complete. If you use a browser extension such as [JShelter](https://jshelter.org/), you will need to [modify your JShelter configuration](./known-broken-extensions.md#jshelter) to allow Anubis' proof of work computation to complete.
## Does Anubis mine Bitcoin?
No. Anubis does not mine Bitcoin.
In order to mine bitcoin, you need to download a copy of the blockchain (so you have the state required to do mining) and also broadcast your mined blocks to the network should you reach a hash with the right number of leading zeroes. You also need to continuously read for newly broadcasted transactions so you can batch them into a block. This requires gigabytes of data to be transferred from the server to the client.
Anubis transfers two digit numbers of kilobytes from the server to the client (which you can independently verify with your browser's Developer Tools feature). This is orders of magnitude below what is required to mine Bitcoin.

View File

@@ -34,6 +34,12 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://bugzilla.proxmox.com - https://bugzilla.proxmox.com
- https://hofstede.io/ - https://hofstede.io/
- https://www.indiemag.fr/ - https://www.indiemag.fr/
- https://reddit.nerdvpn.de/
- https://hosted.weblate.org/
- https://gitea.com/
- https://openwrt.org/
- https://minihoot.site
- https://catgirl.click/
- <details> - <details>
<summary>FreeCAD</summary> <summary>FreeCAD</summary>
- https://forum.freecad.org/ - https://forum.freecad.org/
@@ -57,3 +63,10 @@ This page contains a non-exhaustive list with all websites using Anubis.
<summary>The United Nations</summary> <summary>The United Nations</summary>
- https://policytoolbox.iiep.unesco.org/ - https://policytoolbox.iiep.unesco.org/
</details> </details>
- <details>
<summary>hebis (Alliance of Hessian Libraries)</summary>
- https://ubmr.hds.hebis.de/
- https://tufind.hds.hebis.de/
- https://karla.hds.hebis.de/
- and many more (see https://www.hebis.de/dienste/hebis-discovery-system/)
</details>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

18
go.mod
View File

@@ -3,32 +3,31 @@ module github.com/TecharoHQ/anubis
go 1.24.2 go 1.24.2
require ( require (
github.com/a-h/templ v0.3.865 github.com/a-h/templ v0.3.887
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/cel-go v0.25.0 github.com/google/cel-go v0.25.0
github.com/playwright-community/playwright-go v0.5200.0 github.com/playwright-community/playwright-go v0.5200.0
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.8.0
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
github.com/yl2chen/cidranger v1.0.2 github.com/yl2chen/cidranger v1.0.2
golang.org/x/net v0.40.0 golang.org/x/net v0.40.0
k8s.io/apimachinery v0.33.0 k8s.io/apimachinery v0.33.1
) )
require ( require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect al.essio.dev/pkg/shellescape v1.6.0 // indirect
cel.dev/expr v0.23.1 // indirect cel.dev/expr v0.23.1 // indirect
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/ProtonMail/go-crypto v1.2.0 // indirect
github.com/Songmu/gitconfig v0.2.0 // indirect github.com/Songmu/gitconfig v0.2.0 // indirect
github.com/TecharoHQ/yeet v0.2.3 // indirect github.com/TecharoHQ/yeet v0.6.0 // indirect
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
@@ -42,7 +41,6 @@ require (
github.com/cloudflare/circl v1.6.0 // indirect github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/deckarep/golang-set/v2 v2.7.0 // indirect github.com/deckarep/golang-set/v2 v2.7.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
@@ -61,11 +59,11 @@ require (
github.com/goccy/go-yaml v1.12.0 // indirect github.com/goccy/go-yaml v1.12.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/goreleaser/chglog v0.7.0 // indirect github.com/goreleaser/chglog v0.7.0 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/goreleaser/nfpm/v2 v2.42.0 // indirect github.com/goreleaser/nfpm/v2 v2.42.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
@@ -97,6 +95,7 @@ require (
golang.org/x/sync v0.14.0 // indirect golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.32.0 // indirect golang.org/x/tools v0.32.0 // indirect
golang.org/x/vuln v1.1.4 // indirect golang.org/x/vuln v1.1.4 // indirect
@@ -107,6 +106,7 @@ require (
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.6.1 // indirect honnef.co/go/tools v0.6.1 // indirect
mvdan.cc/sh/v3 v3.11.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect
) )

42
go.sum
View File

@@ -2,8 +2,8 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeX
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
@@ -20,20 +20,20 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s= github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs= github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
github.com/Songmu/gitconfig v0.2.0 h1:pX2++u4KUq+K2k/ZCzGXLtkD3ceCqIdi0tDyb+IbSyo= github.com/Songmu/gitconfig v0.2.0 h1:pX2++u4KUq+K2k/ZCzGXLtkD3ceCqIdi0tDyb+IbSyo=
github.com/Songmu/gitconfig v0.2.0/go.mod h1:cB5bYJer+pl7W8g6RHFwL/0X6aJROVrYuHlvc7PT+hE= github.com/Songmu/gitconfig v0.2.0/go.mod h1:cB5bYJer+pl7W8g6RHFwL/0X6aJROVrYuHlvc7PT+hE=
github.com/TecharoHQ/yeet v0.2.3 h1:Pcsnq5HTnk4Xntlu/FNEidH7x55bIx+f5Mk1hpVIngs= github.com/TecharoHQ/yeet v0.6.0 h1:RCBAjr7wIlllsgy0tpvWpLX7jsZgu2tiuBY3RrprcR0=
github.com/TecharoHQ/yeet v0.2.3/go.mod h1:avLiwxZpNY37A/o35XledvdmGnTkm3G7+Oskxca6Z7Y= github.com/TecharoHQ/yeet v0.6.0/go.mod h1:bj2V4Fg8qKQXoiuPZa3HuawrE8g+LsOQv/9q2WyGSsA=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A= github.com/a-h/templ v0.3.887 h1:QKk7kFzqWGfVwEm/phalqMmZncqnqTrmFEhXHozOXpk=
github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= github.com/a-h/templ v0.3.887/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -46,10 +46,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8= github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8=
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk= github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
@@ -69,6 +65,8 @@ github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps= github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -77,8 +75,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k= github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg= github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
@@ -123,6 +119,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
@@ -147,8 +145,8 @@ github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a h1:JJBdjSfqSy3mnDT0940ASQFghwcZ4y4cb6ttjAoXqwE= github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc h1:qES+d3PvR9CN+zARQQH/bNXH0ybzmdjNMHICrBwXD28=
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -159,8 +157,8 @@ github.com/goreleaser/chglog v0.7.0 h1:/KzXWAeg4DrEz4r3OI6K2Yb8RAsVGeInCUfLWFXL9
github.com/goreleaser/chglog v0.7.0/go.mod h1:2h/yyq9xvTUeM9tOoucBP+jri8Dj28splx+SjlYkklc= github.com/goreleaser/chglog v0.7.0/go.mod h1:2h/yyq9xvTUeM9tOoucBP+jri8Dj28splx+SjlYkklc=
github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I= github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I=
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU= github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
github.com/goreleaser/nfpm/v2 v2.42.0 h1:7BW4WQWyvZDrT0C7SyWop+J8rtqFyTB17Sb2/j/NxMI= github.com/goreleaser/nfpm/v2 v2.42.1 h1:xu2pLRgQuz2ab+YZFoeIzwU/M5jjjCKDGwv1lRbVGvk=
github.com/goreleaser/nfpm/v2 v2.42.0/go.mod h1:DtNL+nKpfB8sMFZp+X7Xu3W64atyZYtTnYe8O925/mg= github.com/goreleaser/nfpm/v2 v2.42.1/go.mod h1:dY53KWYKebkOocxgkmpM7SRX0Nv5hU+jEu2kIaM4/LI=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
@@ -230,8 +228,6 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
@@ -379,8 +375,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ= pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE= pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE=
pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4= pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=

35
internal/gzip.go Normal file
View File

@@ -0,0 +1,35 @@
package internal
import (
"compress/gzip"
"net/http"
"strings"
)
func GzipMiddleware(level int, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz, err := gzip.NewWriterLevel(w, level)
if err != nil {
panic(err)
}
defer gz.Close()
grw := gzipResponseWriter{ResponseWriter: w, sink: gz}
next.ServeHTTP(grw, r)
})
}
type gzipResponseWriter struct {
http.ResponseWriter
sink *gzip.Writer
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.sink.Write(b)
}

7
internal/mimetype.go Normal file
View File

@@ -0,0 +1,7 @@
package internal
import "mime"
func init() {
mime.AddExtensionType(".mjs", "text/javascript")
}

View File

@@ -1,86 +0,0 @@
package valkey
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
valkey "github.com/redis/go-redis/v9"
)
type Store struct {
rdb *valkey.Client
}
func New(rdb *valkey.Client) *Store {
return &Store{rdb: rdb}
}
func (s *Store) Increment(ctx context.Context, segments []string) error {
key := fmt.Sprintf("anubis:%s", strings.Join(segments, ":"))
if err := s.rdb.Incr(ctx, key).Err(); err != nil {
return err
}
return nil
}
func (s *Store) GetInt(ctx context.Context, segments []string) (int, error) {
key := fmt.Sprintf("anubis:%s", strings.Join(segments, ":"))
numStr, err := s.rdb.Get(ctx, key).Result()
if err != nil {
return 0, err
}
num, err := strconv.Atoi(numStr)
if err != nil {
return 0, err
}
return num, nil
}
func (s *Store) MultiGetInt(ctx context.Context, segments [][]string) ([]int, error) {
var keys []string
for _, segment := range segments {
key := fmt.Sprintf("anubis:%s", strings.Join(segment, ":"))
keys = append(keys, key)
}
values, err := s.rdb.MGet(ctx, keys...).Result()
if err != nil {
return nil, err
}
var errs []error
result := make([]int, len(values))
for i, val := range values {
if val == nil {
result[i] = 0
errs = append(errs, fmt.Errorf("can't get key %s: value is null", keys[i]))
continue
}
switch v := val.(type) {
case string:
num, err := strconv.Atoi(v)
if err != nil {
errs = append(errs, fmt.Errorf("can't parse key %s: %w", keys[i], err))
continue
}
result[i] = num
default:
errs = append(errs, fmt.Errorf("can't parse key %s: wanted type string but got type %T", keys[i], val))
}
}
if len(errs) != 0 {
return nil, fmt.Errorf("can't read from valkey: %w", errors.Join(errs...))
}
return result, nil
}

View File

@@ -214,6 +214,11 @@ func TestPlaywrightBrowser(t *testing.T) {
return return
} }
if os.Getenv("SKIP_INTEGRATION") != "" {
t.Skip("SKIP_INTEGRATION was set")
return
}
startPlaywright(t) startPlaywright(t)
pw := setupPlaywright(t) pw := setupPlaywright(t)
@@ -289,6 +294,11 @@ func TestPlaywrightWithBasePrefix(t *testing.T) {
return return
} }
if os.Getenv("SKIP_INTEGRATION") != "" {
t.Skip("SKIP_INTEGRATION was set")
return
}
t.Skip("NOTE(Xe)\\ these tests require HTTPS support in #364") t.Skip("NOTE(Xe)\\ these tests require HTTPS support in #364")
startPlaywright(t) startPlaywright(t)

View File

@@ -3,16 +3,14 @@ package lib
import ( import (
"crypto/ed25519" "crypto/ed25519"
"crypto/sha256" "crypto/sha256"
"crypto/subtle"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"math"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"slices" "slices"
"strconv"
"strings" "strings"
"time" "time"
@@ -26,37 +24,40 @@ import (
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/dnsbl" "github.com/TecharoHQ/anubis/internal/dnsbl"
"github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/internal/ogtags"
"github.com/TecharoHQ/anubis/internal/store/valkey" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/thr1"
// challenge implementations
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
) )
var ( var (
challengesIssued = promauto.NewCounter(prometheus.CounterOpts{ challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_challenges_issued", Name: "anubis_challenges_issued",
Help: "The total number of challenges issued", Help: "The total number of challenges issued",
}) }, []string{"method"})
challengesValidated = promauto.NewCounter(prometheus.CounterOpts{ challengesValidated = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_challenges_validated", Name: "anubis_challenges_validated",
Help: "The total number of challenges validated", Help: "The total number of challenges validated",
}) }, []string{"method"})
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{ droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_dronebl_hits", Name: "anubis_dronebl_hits",
Help: "The total number of hits from DroneBL", Help: "The total number of hits from DroneBL",
}, []string{"status"}) }, []string{"status"})
failedValidations = promauto.NewCounter(prometheus.CounterOpts{ failedValidations = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_failed_validations", Name: "anubis_failed_validations",
Help: "The total number of failed validations", Help: "The total number of failed validations",
}) }, []string{"method"})
timeTaken = promauto.NewHistogram(prometheus.HistogramOpts{ requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_time_taken", Name: "anubis_proxied_requests_total",
Help: "The time taken for a browser to generate a response (milliseconds)", Help: "Number of requests proxied through Anubis to upstream targets",
Buckets: prometheus.ExponentialBucketsRange(1, math.Pow(2, 18), 19), }, []string{"host"})
})
) )
type Server struct { type Server struct {
@@ -69,19 +70,18 @@ type Server struct {
pub ed25519.PublicKey pub ed25519.PublicKey
opts Options opts Options
cookieName string cookieName string
store *valkey.Store
} }
func (s *Server) challengeFor(r *http.Request, difficulty int) string { func (s *Server) challengeFor(r *http.Request, difficulty int) string {
fp := sha256.Sum256(s.priv.Seed()) fp := sha256.Sum256(s.pub[:])
challengeData := fmt.Sprintf( challengeData := fmt.Sprintf(
"Accept-Language=%s,X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d", "THR1=%s,JA4=%s,Fingerprint=%x,User-Agent=%s,WeekTime=%s,Difficulty=%d",
r.Header.Get("Accept-Language"), thr1.Fingerprint(r),
r.Header.Get("X-Real-Ip"), r.Header.Get("X-Tls-Fingerprint-Ja4"),
fp,
r.UserAgent(), r.UserAgent(),
time.Now().UTC().Round(24*7*time.Hour).Format(time.RFC3339), time.Now().UTC().Round(24*7*time.Hour).Format(time.RFC3339),
fp,
difficulty, difficulty,
) )
return internal.SHA256sum(challengeData) return internal.SHA256sum(challengeData)
@@ -98,6 +98,12 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) { func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
lg := internal.GetRequestLogger(r) lg := internal.GetRequestLogger(r)
// Adjust cookie path if base prefix is not empty
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
cr, rule, err := s.check(r) cr, rule, err := s.check(r)
if err != nil { if err != nil {
lg.Error("check failed", "err", err) lg.Error("check failed", "err", err)
@@ -123,21 +129,21 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
ckie, err := r.Cookie(s.cookieName) ckie, err := r.Cookie(s.cookieName)
if err != nil { if err != nil {
lg.Debug("cookie not found", "path", r.URL.Path) lg.Debug("cookie not found", "path", r.URL.Path)
s.ClearCookie(w) s.ClearCookie(w, s.cookieName, cookiePath)
s.RenderIndex(w, r, rule, httpStatusOnly) s.RenderIndex(w, r, rule, httpStatusOnly)
return return
} }
if err := ckie.Valid(); err != nil { if err := ckie.Valid(); err != nil {
lg.Debug("cookie is invalid", "err", err) lg.Debug("cookie is invalid", "err", err)
s.ClearCookie(w) s.ClearCookie(w, s.cookieName, cookiePath)
s.RenderIndex(w, r, rule, httpStatusOnly) s.RenderIndex(w, r, rule, httpStatusOnly)
return return
} }
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() { if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
lg.Debug("cookie expired", "path", r.URL.Path) lg.Debug("cookie expired", "path", r.URL.Path)
s.ClearCookie(w) s.ClearCookie(w, s.cookieName, cookiePath)
s.RenderIndex(w, r, rule, httpStatusOnly) s.RenderIndex(w, r, rule, httpStatusOnly)
return return
} }
@@ -148,7 +154,30 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
if err != nil || !token.Valid { if err != nil || !token.Valid {
lg.Debug("invalid token", "path", r.URL.Path, "err", err) lg.Debug("invalid token", "path", r.URL.Path, "err", err)
s.ClearCookie(w) s.ClearCookie(w, s.cookieName, cookiePath)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
lg.Debug("invalid token claims type", "path", r.URL.Path)
s.ClearCookie(w, s.cookieName, cookiePath)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
policyRule, ok := claims["policyRule"].(string)
if !ok {
lg.Debug("policyRule claim is not a string")
s.ClearCookie(w, s.cookieName, cookiePath)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
if policyRule != rule.Hash() {
lg.Debug("user originally passed with a different rule, issuing new challenge", "old", policyRule, "new", rule.Name)
s.ClearCookie(w, s.cookieName, cookiePath)
s.RenderIndex(w, r, rule, httpStatusOnly) s.RenderIndex(w, r, rule, httpStatusOnly)
return return
} }
@@ -158,13 +187,19 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
} }
func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool { func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool {
// Adjust cookie path if base prefix is not empty
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
switch cr.Rule { switch cr.Rule {
case config.RuleAllow: case config.RuleAllow:
lg.Debug("allowing traffic to origin (explicit)") lg.Debug("allowing traffic to origin (explicit)")
s.ServeHTTPNext(w, r) s.ServeHTTPNext(w, r)
return true return true
case config.RuleDeny: case config.RuleDeny:
s.ClearCookie(w) s.ClearCookie(w, s.cookieName, cookiePath)
lg.Info("explicit deny") lg.Info("explicit deny")
if rule == nil { if rule == nil {
lg.Error("rule is nil, cannot calculate checksum") lg.Error("rule is nil, cannot calculate checksum")
@@ -183,7 +218,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
s.RenderBench(w, r) s.RenderBench(w, r)
return true return true
default: default:
s.ClearCookie(w) s.ClearCookie(w, s.cookieName, cookiePath)
slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule) slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule)
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"") s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"")
return true return true
@@ -216,6 +251,21 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
lg := internal.GetRequestLogger(r) lg := internal.GetRequestLogger(r)
redir := r.FormValue("redir")
if redir == "" {
w.WriteHeader(http.StatusBadRequest)
encoder := json.NewEncoder(w)
lg.Error("invalid invocation of MakeChallenge", "redir", redir)
encoder.Encode(struct {
Error string `json:"error"`
}{
Error: "Invalid invocation of MakeChallenge",
})
return
}
r.URL.Path = redir
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
cr, rule, err := s.check(r) cr, rule, err := s.check(r)
if err != nil { if err != nil {
@@ -235,9 +285,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
lg = lg.With("check_result", cr) lg = lg.With("check_result", cr)
challenge := s.challengeFor(r, rule.Challenge.Difficulty) challenge := s.challengeFor(r, rule.Challenge.Difficulty)
if s.store != nil { s.SetCookie(w, anubis.TestCookieName, challenge, "/")
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "challenges_issued"})
}
err = encoder.Encode(struct { err = encoder.Encode(struct {
Rules *config.ChallengeRules `json:"rules"` Rules *config.ChallengeRules `json:"rules"`
@@ -252,12 +300,28 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
return return
} }
lg.Debug("made challenge", "challenge", challenge, "rules", rule.Challenge, "cr", cr) lg.Debug("made challenge", "challenge", challenge, "rules", rule.Challenge, "cr", cr)
challengesIssued.Inc() challengesIssued.WithLabelValues("api").Inc()
} }
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
lg := internal.GetRequestLogger(r) lg := internal.GetRequestLogger(r)
// Adjust cookie path if base prefix is not empty
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
if _, err := r.Cookie(anubis.TestCookieName); err == http.ErrNoCookie {
s.ClearCookie(w, s.cookieName, cookiePath)
s.ClearCookie(w, anubis.TestCookieName, "/")
lg.Warn("user has cookies disabled, this is not an anubis bug")
s.respondWithError(w, r, "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain")
return
}
s.ClearCookie(w, anubis.TestCookieName, "/")
redir := r.FormValue("redir") redir := r.FormValue("redir")
redirURL, err := url.ParseRequestURI(redir) redirURL, err := url.ParseRequestURI(redir)
if err != nil { if err != nil {
@@ -268,42 +332,6 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
// used by the path checker rule // used by the path checker rule
r.URL = redirURL r.URL = redirURL
cr, rule, err := s.check(r)
if err != nil {
lg.Error("check failed", "err", err)
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".\"")
return
}
lg = lg.With("check_result", cr)
nonceStr := r.FormValue("nonce")
if nonceStr == "" {
s.ClearCookie(w)
lg.Debug("no nonce")
s.respondWithError(w, r, "missing nonce")
return
}
elapsedTimeStr := r.FormValue("elapsedTime")
if elapsedTimeStr == "" {
s.ClearCookie(w)
lg.Debug("no elapsedTime")
s.respondWithError(w, r, "missing elapsedTime")
return
}
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
if err != nil {
s.ClearCookie(w)
lg.Debug("elapsedTime doesn't parse", "err", err)
s.respondWithError(w, r, "invalid elapsedTime")
return
}
lg.Info("challenge took", "elapsedTime", elapsedTime)
timeTaken.Observe(elapsedTime)
response := r.FormValue("response")
urlParsed, err := r.URL.Parse(redir) urlParsed, err := r.URL.Parse(redir)
if err != nil { if err != nil {
s.respondWithError(w, r, "Redirect URL not parseable") s.respondWithError(w, r, "Redirect URL not parseable")
@@ -314,79 +342,57 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
return return
} }
challenge := s.challengeFor(r, rule.Challenge.Difficulty) cr, rule, err := s.check(r)
nonce, err := strconv.Atoi(nonceStr)
if err != nil { if err != nil {
s.ClearCookie(w) lg.Error("check failed", "err", err)
lg.Debug("nonce doesn't parse", "err", err) s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\"")
s.respondWithError(w, r, "invalid nonce") return
}
lg = lg.With("check_result", cr)
impl, ok := challenge.Get(rule.Challenge.Algorithm)
if !ok {
lg.Error("check failed", "err", err)
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
return return
} }
calcString := fmt.Sprintf("%s%d", challenge, nonce) challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
calculated := internal.SHA256sum(calcString)
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 { if err := impl.Validate(r, lg, rule, challengeStr); err != nil {
s.ClearCookie(w) failedValidations.WithLabelValues(string(rule.Challenge.Algorithm)).Inc()
lg.Debug("hash does not match", "got", response, "want", calculated) var cerr *challenge.Error
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden) s.ClearCookie(w, s.cookieName, cookiePath)
if s.store != nil { lg.Debug("challenge validate call failed", "err", err)
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "fail"})
switch {
case errors.As(err, &cerr):
switch {
case errors.Is(err, challenge.ErrFailed):
s.respondWithStatus(w, r, cerr.PublicReason, cerr.StatusCode)
case errors.Is(err, challenge.ErrInvalidFormat), errors.Is(err, challenge.ErrMissingField):
s.respondWithError(w, r, cerr.PublicReason)
}
} }
failedValidations.Inc()
return
} }
// compare the leading zeroes
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
s.ClearCookie(w)
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
if s.store != nil {
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "fail"})
}
failedValidations.Inc()
return
}
// Adjust cookie path if base prefix is not empty
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
// generate JWT cookie // generate JWT cookie
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{ tokenString, err := s.signJWT(jwt.MapClaims{
"challenge": challenge, "challenge": challengeStr,
"nonce": nonceStr, "method": rule.Challenge.Algorithm,
"response": response, "policyRule": rule.Hash(),
"iat": time.Now().Unix(), "action": string(cr.Rule),
"nbf": time.Now().Add(-1 * time.Minute).Unix(),
"exp": time.Now().Add(s.opts.CookieExpiration).Unix(),
}) })
tokenString, err := token.SignedString(s.priv)
if err != nil { if err != nil {
lg.Error("failed to sign JWT", "err", err) lg.Error("failed to sign JWT", "err", err)
s.ClearCookie(w) s.ClearCookie(w, s.cookieName, cookiePath)
s.respondWithError(w, r, "failed to sign JWT") s.respondWithError(w, r, "failed to sign JWT")
return return
} }
http.SetCookie(w, &http.Cookie{ s.SetCookie(w, s.cookieName, tokenString, cookiePath)
Name: s.cookieName,
Value: tokenString,
Expires: time.Now().Add(s.opts.CookieExpiration),
SameSite: http.SameSiteLaxMode,
Domain: s.opts.CookieDomain,
Partitioned: s.opts.CookiePartitioned,
Path: cookiePath,
})
if s.store != nil { challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "pass"})
}
challengesValidated.Inc()
lg.Debug("challenge passed, redirecting to app") lg.Debug("challenge passed, redirecting to app")
http.Redirect(w, r, redir, http.StatusFound) http.Redirect(w, r, redir, http.StatusFound)
} }
@@ -415,8 +421,6 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
return decaymap.Zilch[policy.CheckResult](), nil, fmt.Errorf("[misconfiguration] %q is not an IP address", host) return decaymap.Zilch[policy.CheckResult](), nil, fmt.Errorf("[misconfiguration] %q is not an IP address", host)
} }
weight := 0
for _, b := range s.policy.Bots { for _, b := range s.policy.Bots {
match, err := b.Rules.Check(r) match, err := b.Rules.Check(r)
if err != nil { if err != nil {
@@ -424,33 +428,17 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
} }
if match { if match {
switch b.Action { return cr("bot/"+b.Name, b.Action), &b, nil
case config.RuleDeny, config.RuleAllow, config.RuleBenchmark:
return cr("bot/"+b.Name, b.Action), &b, nil
case config.RuleChallenge:
weight += 5
case config.RuleWeigh:
weight += b.Weight.Adjust
}
} }
} }
if weight < 0 {
return cr("weight/okay", config.RuleAllow), &policy.Bot{
Challenge: &config.ChallengeRules{
Difficulty: s.policy.DefaultDifficulty,
ReportAs: s.policy.DefaultDifficulty,
Algorithm: config.AlgorithmFast,
},
}, nil
}
return cr("default/allow", config.RuleAllow), &policy.Bot{ return cr("default/allow", config.RuleAllow), &policy.Bot{
Challenge: &config.ChallengeRules{ Challenge: &config.ChallengeRules{
Difficulty: s.policy.DefaultDifficulty, Difficulty: s.policy.DefaultDifficulty,
ReportAs: s.policy.DefaultDifficulty, ReportAs: s.policy.DefaultDifficulty,
Algorithm: config.AlgorithmFast, Algorithm: config.DefaultAlgorithm,
}, },
Rules: &policy.CheckerList{},
}, nil }, nil
} }

View File

@@ -5,8 +5,10 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"os" "os"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@@ -17,6 +19,10 @@ import (
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
) )
func init() {
internal.InitSlog("debug")
}
func loadPolicies(t *testing.T, fname string) *policy.ParsedConfig { func loadPolicies(t *testing.T, fname string) *policy.ParsedConfig {
t.Helper() t.Helper()
@@ -39,20 +45,29 @@ func spawnAnubis(t *testing.T, opts Options) *Server {
return s return s
} }
type challenge struct { type challengeResp struct {
Challenge string `json:"challenge"` Challenge string `json:"challenge"`
} }
func makeChallenge(t *testing.T, ts *httptest.Server) challenge { func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challengeResp {
t.Helper() t.Helper()
resp, err := ts.Client().Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil) req, err := http.NewRequest(http.MethodPost, ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", nil)
if err != nil {
t.Fatalf("can't make request: %v", err)
}
q := req.URL.Query()
q.Set("redir", "/")
req.URL.RawQuery = q.Encode()
resp, err := cli.Do(req)
if err != nil { if err != nil {
t.Fatalf("can't request challenge: %v", err) t.Fatalf("can't request challenge: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
var chall challenge var chall challengeResp
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil { if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
t.Fatalf("can't read challenge response body: %v", err) t.Fatalf("can't read challenge response body: %v", err)
} }
@@ -60,6 +75,86 @@ func makeChallenge(t *testing.T, ts *httptest.Server) challenge {
return chall return chall
} }
func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.Client, chall challengeResp) *http.Response {
t.Helper()
nonce := 0
elapsedTime := 420
redir := "/"
calculated := ""
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
calculated = internal.SHA256sum(calcString)
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
if err != nil {
t.Fatalf("can't make request: %v", err)
}
q := req.URL.Query()
q.Set("response", calculated)
q.Set("nonce", fmt.Sprint(nonce))
q.Set("redir", redir)
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
req.URL.RawQuery = q.Encode()
resp, err := cli.Do(req)
if err != nil {
t.Fatalf("can't do request: %v", err)
}
return resp
}
type loggingCookieJar struct {
t *testing.T
lock sync.Mutex
cookies map[string][]*http.Cookie
}
func (lcj *loggingCookieJar) Cookies(u *url.URL) []*http.Cookie {
lcj.lock.Lock()
defer lcj.lock.Unlock()
// XXX(Xe): This is not RFC compliant in the slightest.
result, ok := lcj.cookies[u.Host]
if !ok {
return nil
}
lcj.t.Logf("requested cookies for %s", u)
for _, ckie := range result {
lcj.t.Logf("get cookie: <- %s", ckie)
}
return result
}
func (lcj *loggingCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
lcj.lock.Lock()
defer lcj.lock.Unlock()
for _, ckie := range cookies {
lcj.t.Logf("set cookie: %s -> %s", u, ckie)
}
// XXX(Xe): This is not RFC compliant in the slightest.
lcj.cookies[u.Host] = append(lcj.cookies[u.Host], cookies...)
}
func httpClient(t *testing.T) *http.Client {
t.Helper()
cli := &http.Client{
Jar: &loggingCookieJar{t: t, cookies: map[string][]*http.Cookie{}},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return cli
}
func TestLoadPolicies(t *testing.T) { func TestLoadPolicies(t *testing.T) {
for _, fname := range []string{"botPolicies.json", "botPolicies.yaml"} { for _, fname := range []string{"botPolicies.json", "botPolicies.yaml"} {
t.Run(fname, func(t *testing.T) { t.Run(fname, func(t *testing.T) {
@@ -85,42 +180,15 @@ func TestCVE2025_24369(t *testing.T) {
Next: http.NewServeMux(), Next: http.NewServeMux(),
Policy: pol, Policy: pol,
CookieDomain: ".local.cetacean.club", CookieName: t.Name(),
CookiePartitioned: true,
CookieName: t.Name(),
}) })
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv)) ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
defer ts.Close() defer ts.Close()
chall := makeChallenge(t, ts) cli := httpClient(t)
calcString := fmt.Sprintf("%s%d", chall.Challenge, 0) chall := makeChallenge(t, ts, cli)
calculated := internal.SHA256sum(calcString) resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
nonce := 0
elapsedTime := 420
redir := "/"
cli := ts.Client()
cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
if err != nil {
t.Fatalf("can't make request: %v", err)
}
q := req.URL.Query()
q.Set("response", calculated)
q.Set("nonce", fmt.Sprint(nonce))
q.Set("redir", redir)
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
req.URL.RawQuery = q.Encode()
resp, err := cli.Do(req)
if err != nil {
t.Fatalf("can't do challenge passing")
}
if resp.StatusCode == http.StatusFound { if resp.StatusCode == http.StatusFound {
t.Log("Regression on CVE-2025-24369") t.Log("Regression on CVE-2025-24369")
@@ -137,58 +205,18 @@ func TestCookieCustomExpiration(t *testing.T) {
Next: http.NewServeMux(), Next: http.NewServeMux(),
Policy: pol, Policy: pol,
CookieDomain: "local.cetacean.club",
CookieName: t.Name(),
CookieExpiration: ckieExpiration, CookieExpiration: ckieExpiration,
}) })
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv)) ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
defer ts.Close() defer ts.Close()
cli := &http.Client{ cli := httpClient(t)
CheckRedirect: func(req *http.Request, via []*http.Request) error { chall := makeChallenge(t, ts, cli)
return http.ErrUseLastResponse
},
}
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil) requestReceiveLowerBound := time.Now().Add(-1 * time.Minute)
if err != nil { resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
t.Fatalf("can't request challenge: %v", err)
}
defer resp.Body.Close()
var chall = struct {
Challenge string `json:"challenge"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
t.Fatalf("can't read challenge response body: %v", err)
}
nonce := 0
elapsedTime := 420
redir := "/"
calculated := ""
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
calculated = internal.SHA256sum(calcString)
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
if err != nil {
t.Fatalf("can't make request: %v", err)
}
q := req.URL.Query()
q.Set("response", calculated)
q.Set("nonce", fmt.Sprint(nonce))
q.Set("redir", redir)
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
req.URL.RawQuery = q.Encode()
requestReceiveLowerBound := time.Now()
resp, err = cli.Do(req)
requestReceiveUpperBound := time.Now() requestReceiveUpperBound := time.Now()
if err != nil {
t.Fatalf("can't do challenge passing")
}
if resp.StatusCode != http.StatusFound { if resp.StatusCode != http.StatusFound {
resp.Write(os.Stderr) resp.Write(os.Stderr)
@@ -226,59 +254,21 @@ func TestCookieSettings(t *testing.T) {
Next: http.NewServeMux(), Next: http.NewServeMux(),
Policy: pol, Policy: pol,
CookieDomain: "local.cetacean.club", CookieDomain: "127.0.0.1",
CookiePartitioned: true, CookiePartitioned: true,
CookieName: t.Name(), CookieName: t.Name(),
CookieExpiration: anubis.CookieDefaultExpirationTime, CookieExpiration: anubis.CookieDefaultExpirationTime,
}) })
requestReceiveLowerBound := time.Now()
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv)) ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
defer ts.Close() defer ts.Close()
cli := &http.Client{ cli := httpClient(t)
CheckRedirect: func(req *http.Request, via []*http.Request) error { chall := makeChallenge(t, ts, cli)
return http.ErrUseLastResponse
},
}
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil) resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
if err != nil {
t.Fatalf("can't request challenge: %v", err)
}
defer resp.Body.Close()
var chall = struct {
Challenge string `json:"challenge"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
t.Fatalf("can't read challenge response body: %v", err)
}
nonce := 0
elapsedTime := 420
redir := "/"
calculated := ""
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
calculated = internal.SHA256sum(calcString)
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
if err != nil {
t.Fatalf("can't make request: %v", err)
}
q := req.URL.Query()
q.Set("response", calculated)
q.Set("nonce", fmt.Sprint(nonce))
q.Set("redir", redir)
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
req.URL.RawQuery = q.Encode()
requestReceiveLowerBound := time.Now()
resp, err = cli.Do(req)
requestReceiveUpperBound := time.Now() requestReceiveUpperBound := time.Now()
if err != nil {
t.Fatalf("can't do challenge passing")
}
if resp.StatusCode != http.StatusFound { if resp.StatusCode != http.StatusFound {
resp.Write(os.Stderr) resp.Write(os.Stderr)
@@ -298,8 +288,8 @@ func TestCookieSettings(t *testing.T) {
return return
} }
if ckie.Domain != "local.cetacean.club" { if ckie.Domain != "127.0.0.1" {
t.Errorf("cookie domain is wrong, wanted local.cetacean.club, got: %s", ckie.Domain) t.Errorf("cookie domain is wrong, wanted 127.0.0.1, got: %s", ckie.Domain)
} }
expirationLowerBound := requestReceiveLowerBound.Add(anubis.CookieDefaultExpirationTime) expirationLowerBound := requestReceiveLowerBound.Add(anubis.CookieDefaultExpirationTime)
@@ -373,7 +363,7 @@ func TestBasePrefix(t *testing.T) {
}{ }{
{ {
name: "no prefix", name: "no prefix",
basePrefix: "", basePrefix: "/",
path: "/.within.website/x/cmd/anubis/api/make-challenge", path: "/.within.website/x/cmd/anubis/api/make-challenge",
expected: "/.within.website/x/cmd/anubis/api/make-challenge", expected: "/.within.website/x/cmd/anubis/api/make-challenge",
}, },
@@ -408,8 +398,19 @@ func TestBasePrefix(t *testing.T) {
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv)) ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
defer ts.Close() defer ts.Close()
cli := httpClient(t)
req, err := http.NewRequest(http.MethodPost, ts.URL+tc.path, nil)
if err != nil {
t.Fatal(err)
}
q := req.URL.Query()
q.Set("redir", tc.basePrefix)
req.URL.RawQuery = q.Encode()
// Test API endpoint with prefix // Test API endpoint with prefix
resp, err := ts.Client().Post(ts.URL+tc.path, "", nil) resp, err := cli.Do(req)
if err != nil { if err != nil {
t.Fatalf("can't request challenge: %v", err) t.Fatalf("can't request challenge: %v", err)
} }
@@ -419,7 +420,7 @@ func TestBasePrefix(t *testing.T) {
t.Errorf("expected status code %d, got: %d", http.StatusOK, resp.StatusCode) t.Errorf("expected status code %d, got: %d", http.StatusOK, resp.StatusCode)
} }
var chall challenge var chall challengeResp
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil { if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
t.Fatalf("can't read challenge response body: %v", err) t.Fatalf("can't read challenge response body: %v", err)
} }
@@ -443,7 +444,6 @@ func TestBasePrefix(t *testing.T) {
elapsedTime := 420 elapsedTime := 420
redir := "/" redir := "/"
cli := ts.Client()
cli.CheckRedirect = func(req *http.Request, via []*http.Request) error { cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
} }
@@ -452,12 +452,16 @@ func TestBasePrefix(t *testing.T) {
passChallengePath := tc.path passChallengePath := tc.path
passChallengePath = passChallengePath[:strings.LastIndex(passChallengePath, "/")+1] + "pass-challenge" passChallengePath = passChallengePath[:strings.LastIndex(passChallengePath, "/")+1] + "pass-challenge"
req, err := http.NewRequest(http.MethodGet, ts.URL+passChallengePath, nil) req, err = http.NewRequest(http.MethodGet, ts.URL+passChallengePath, nil)
if err != nil { if err != nil {
t.Fatalf("can't make request: %v", err) t.Fatalf("can't make request: %v", err)
} }
q := req.URL.Query() for _, ckie := range resp.Cookies() {
req.AddCookie(ckie)
}
q = req.URL.Query()
q.Set("response", calculated) q.Set("response", calculated)
q.Set("nonce", fmt.Sprint(nonce)) q.Set("nonce", fmt.Sprint(nonce))
q.Set("redir", redir) q.Set("redir", redir)
@@ -561,6 +565,25 @@ func TestCloudflareWorkersRule(t *testing.T) {
t.Fatalf("can't construct libanubis.Server: %v", err) t.Fatalf("can't construct libanubis.Server: %v", err)
} }
t.Run("with-cf-worker-header", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Add("X-Real-Ip", "127.0.0.1")
req.Header.Add("Cf-Worker", "true")
cr, _, err := s.check(req)
if err != nil {
t.Fatal(err)
}
if cr.Rule != config.RuleDeny {
t.Errorf("rule is wrong, wanted %s, got: %s", config.RuleDeny, cr.Rule)
}
})
t.Run("no-cf-worker-header", func(t *testing.T) { t.Run("no-cf-worker-header", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/", nil) req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil { if err != nil {
@@ -581,3 +604,31 @@ func TestCloudflareWorkersRule(t *testing.T) {
}) })
} }
} }
func TestRuleChange(t *testing.T) {
pol := loadPolicies(t, "testdata/rule_change.yaml")
pol.DefaultDifficulty = 0
ckieExpiration := 10 * time.Minute
srv := spawnAnubis(t, Options{
Next: http.NewServeMux(),
Policy: pol,
CookieDomain: "127.0.0.1",
CookieName: t.Name(),
CookieExpiration: ckieExpiration,
})
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
defer ts.Close()
cli := httpClient(t)
chall := makeChallenge(t, ts, cli)
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
if resp.StatusCode != http.StatusFound {
resp.Write(os.Stderr)
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
}
}

View File

@@ -0,0 +1,47 @@
package challenge
import (
"log/slog"
"net/http"
"sort"
"sync"
"github.com/TecharoHQ/anubis/lib/policy"
"github.com/a-h/templ"
)
var (
registry map[string]Impl = map[string]Impl{}
regLock sync.RWMutex
)
func Register(name string, impl Impl) {
regLock.Lock()
defer regLock.Unlock()
registry[name] = impl
}
func Get(name string) (Impl, bool) {
regLock.RLock()
defer regLock.RUnlock()
result, ok := registry[name]
return result, ok
}
func Methods() []string {
regLock.RLock()
defer regLock.RUnlock()
var result []string
for method := range registry {
result = append(result, method)
}
sort.Strings(result)
return result
}
type Impl interface {
Fail(w http.ResponseWriter, r *http.Request) error
Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error)
Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error
}

37
lib/challenge/error.go Normal file
View File

@@ -0,0 +1,37 @@
package challenge
import (
"errors"
"fmt"
"net/http"
)
var (
ErrFailed = errors.New("challenge: user failed challenge")
ErrMissingField = errors.New("challenge: missing field")
ErrInvalidFormat = errors.New("challenge: field has invalid format")
)
func NewError(verb, publicReason string, privateReason error) *Error {
return &Error{
Verb: verb,
PublicReason: publicReason,
PrivateReason: privateReason,
StatusCode: http.StatusForbidden,
}
}
type Error struct {
Verb string
PublicReason string
PrivateReason error
StatusCode int
}
func (e *Error) Error() string {
return fmt.Sprintf("challenge: error when processing challenge: %s: %v", e.Verb, e.PrivateReason)
}
func (e *Error) Unwrap() error {
return e.PrivateReason
}

14
lib/challenge/metrics.go Normal file
View File

@@ -0,0 +1,14 @@
package challenge
import (
"math"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var TimeTaken = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "anubis_time_taken",
Help: "The time taken for a browser to generate a response (milliseconds)",
Buckets: prometheus.ExponentialBucketsRange(1, math.Pow(2, 20), 20),
}, []string{"method"})

View File

@@ -0,0 +1,83 @@
package proofofwork
import (
"crypto/subtle"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
"github.com/TecharoHQ/anubis/internal"
chall "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/web"
"github.com/a-h/templ"
)
func init() {
chall.Register("fast", &Impl{Algorithm: "fast"})
chall.Register("slow", &Impl{Algorithm: "slow"})
}
type Impl struct {
Algorithm string
}
func (i *Impl) Fail(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error) {
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
if err != nil {
return nil, fmt.Errorf("can't render page: %w", err)
}
return component, nil
}
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error {
nonceStr := r.FormValue("nonce")
if nonceStr == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
}
nonce, err := strconv.Atoi(nonceStr)
if err != nil {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
}
elapsedTimeStr := r.FormValue("elapsedTime")
if elapsedTimeStr == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w elapsedTime", chall.ErrMissingField))
}
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
if err != nil {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: elapsedTime: %w", chall.ErrInvalidFormat, err))
}
response := r.FormValue("response")
if response == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
}
calcString := fmt.Sprintf("%s%d", challenge, nonce)
calculated := internal.SHA256sum(calcString)
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
}
// compare the leading zeroes
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
}
lg.Debug("challenge took", "elapsedTime", elapsedTime)
chall.TimeTaken.WithLabelValues(i.Algorithm).Observe(elapsedTime)
return nil
}

View File

@@ -0,0 +1,136 @@
package proofofwork
import (
"errors"
"log/slog"
"net/http"
"testing"
"github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config"
)
func mkRequest(t *testing.T, values map[string]string) *http.Request {
t.Helper()
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}
q := req.URL.Query()
for k, v := range values {
q.Set(k, v)
}
req.URL.RawQuery = q.Encode()
return req
}
func TestBasic(t *testing.T) {
i := &Impl{Algorithm: "fast"}
bot := &policy.Bot{
Challenge: &config.ChallengeRules{
Algorithm: "fast",
Difficulty: 0,
ReportAs: 0,
},
}
const challengeStr = "hunter"
const response = "2652bdba8fb4d2ab39ef28d8534d7694c557a4ae146c1e9237bd8d950280500e"
for _, cs := range []struct {
name string
req *http.Request
err error
challengeStr string
}{
{
name: "allgood",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "69",
"response": response,
}),
err: nil,
challengeStr: challengeStr,
},
{
name: "no-params",
req: mkRequest(t, map[string]string{}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "missing-nonce",
req: mkRequest(t, map[string]string{
"elapsedTime": "69",
"response": response,
}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "missing-elapsedTime",
req: mkRequest(t, map[string]string{
"nonce": "0",
"response": response,
}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "missing-response",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "69",
}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "wrong-nonce-format",
req: mkRequest(t, map[string]string{
"nonce": "taco",
"elapsedTime": "69",
"response": response,
}),
err: challenge.ErrInvalidFormat,
challengeStr: challengeStr,
},
{
name: "wrong-elapsedTime-format",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "taco",
"response": response,
}),
err: challenge.ErrInvalidFormat,
challengeStr: challengeStr,
},
{
name: "invalid-response",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "69",
"response": response,
}),
err: challenge.ErrFailed,
challengeStr: "Tacos are tasty",
},
} {
t.Run(cs.name, func(t *testing.T) {
lg := slog.With()
if _, err := i.Issue(cs.req, lg, bot, cs.challengeStr, nil); err != nil {
t.Errorf("can't issue challenge: %v", err)
}
if err := i.Validate(cs.req, lg, bot, cs.challengeStr); !errors.Is(err, cs.err) {
t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
}
})
}
}

View File

@@ -3,6 +3,7 @@ package lib
import ( import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"errors"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
@@ -17,6 +18,7 @@ import (
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/dnsbl" "github.com/TecharoHQ/anubis/internal/dnsbl"
"github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/internal/ogtags"
"github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/web" "github.com/TecharoHQ/anubis/web"
"github.com/TecharoHQ/anubis/xess" "github.com/TecharoHQ/anubis/xess"
@@ -65,6 +67,17 @@ func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedC
}(fin) }(fin)
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty) anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
var validationErrs []error
for _, b := range anubisPolicy.Bots {
if _, ok := challenge.Get(b.Challenge.Algorithm); !ok {
validationErrs = append(validationErrs, fmt.Errorf("%w %s", policy.ErrChallengeRuleHasWrongAlgorithm, b.Challenge.Algorithm))
}
}
if len(validationErrs) != 0 {
return nil, fmt.Errorf("can't do final validation of Anubis config: %w", errors.Join(validationErrs...))
}
return anubisPolicy, err return anubisPolicy, err
} }

51
lib/config_test.go Normal file
View File

@@ -0,0 +1,51 @@
package lib
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/lib/policy"
)
func TestInvalidChallengeMethod(t *testing.T) {
if _, err := LoadPoliciesOrDefault("testdata/invalid-challenge-method.yaml", 4); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
}
}
func TestBadConfigs(t *testing.T) {
finfos, err := os.ReadDir("policy/config/testdata/bad")
if err != nil {
t.Fatal(err)
}
for _, st := range finfos {
st := st
t.Run(st.Name(), func(t *testing.T) {
if _, err := LoadPoliciesOrDefault(filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err == nil {
t.Fatal(err)
} else {
t.Log(err)
}
})
}
}
func TestGoodConfigs(t *testing.T) {
finfos, err := os.ReadDir("policy/config/testdata/good")
if err != nil {
t.Fatal(err)
}
for _, st := range finfos {
st := st
t.Run(st.Name(), func(t *testing.T) {
if _, err := LoadPoliciesOrDefault(filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err != nil {
t.Fatal(err)
}
})
}
}

View File

@@ -1,24 +1,44 @@
package lib package lib
import ( import (
"fmt"
"math/rand"
"net/http" "net/http"
"slices" "slices"
"strings"
"time" "time"
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/web" "github.com/TecharoHQ/anubis/web"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/golang-jwt/jwt/v5"
) )
func (s *Server) ClearCookie(w http.ResponseWriter) { func (s *Server) SetCookie(w http.ResponseWriter, name, value, path string) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: s.cookieName, Name: name,
Value: "", Value: value,
Expires: time.Now().Add(-1 * time.Hour), Expires: time.Now().Add(s.opts.CookieExpiration),
MaxAge: -1, SameSite: http.SameSiteLaxMode,
SameSite: http.SameSiteLaxMode, Domain: s.opts.CookieDomain,
Domain: s.opts.CookieDomain, Partitioned: s.opts.CookiePartitioned,
Path: path,
})
}
func (s *Server) ClearCookie(w http.ResponseWriter, name, path string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: "",
MaxAge: -1,
Expires: time.Now().Add(-1 * time.Minute),
SameSite: http.SameSiteLaxMode,
Partitioned: s.opts.CookiePartitioned,
Domain: s.opts.CookieDomain,
Path: path,
}) })
} }
@@ -38,6 +58,10 @@ func (t UnixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return t.Transport.RoundTrip(req) return t.Transport.RoundTrip(req)
} }
func randomChance(n int) bool {
return rand.Intn(n) == 0
}
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) { func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
if returnHTTPStatusOnly { if returnHTTPStatusOnly {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
@@ -47,7 +71,13 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
lg := internal.GetRequestLogger(r) lg := internal.GetRequestLogger(r)
challenge := s.challengeFor(r, rule.Challenge.Difficulty) if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
lg.Error("client was given a challenge but does not in fact support gzip compression")
s.respondWithError(w, r, "Client Error: Please ensure your browser is up to date and try again later.")
}
challengesIssued.WithLabelValues("embedded").Add(1)
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
var ogTags map[string]string = nil var ogTags map[string]string = nil
if s.opts.OGPassthrough { if s.opts.OGPassthrough {
@@ -58,21 +88,31 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
} }
} }
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags) http.SetCookie(w, &http.Cookie{
Name: anubis.TestCookieName,
Value: challengeStr,
Expires: time.Now().Add(30 * time.Minute),
Path: "/",
})
impl, ok := challenge.Get(rule.Challenge.Algorithm)
if !ok {
lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm)
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
return
}
component, err := impl.Issue(r, lg, rule, challengeStr, ogTags)
if err != nil { if err != nil {
lg.Error("render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this. lg.Error("[unexpected] render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"") s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"")
return return
} }
if s.store != nil { handler := internal.GzipMiddleware(1, internal.NoStoreCache(templ.Handler(
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "challenges_issued"})
}
handler := internal.NoStoreCache(templ.Handler(
component, component,
templ.WithStatus(s.opts.Policy.StatusCodes.Challenge), templ.WithStatus(s.opts.Policy.StatusCodes.Challenge),
)) )))
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
} }
@@ -117,6 +157,15 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
web.Base("You are not a bot!", web.StaticHappy()), web.Base("You are not a bot!", web.StaticHappy()),
).ServeHTTP(w, r) ).ServeHTTP(w, r)
} else { } else {
requestsProxied.WithLabelValues(r.Host).Inc()
s.next.ServeHTTP(w, r) s.next.ServeHTTP(w, r)
} }
} }
func (s *Server) signJWT(claims jwt.MapClaims) (string, error) {
claims["iat"] = time.Now().Unix()
claims["nbf"] = time.Now().Add(-1 * time.Minute).Unix()
claims["exp"] = time.Now().Add(s.opts.CookieExpiration).Unix()
return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.priv)
}

View File

@@ -11,7 +11,7 @@ func TestClearCookie(t *testing.T) {
srv := spawnAnubis(t, Options{}) srv := spawnAnubis(t, Options{})
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
srv.ClearCookie(rw) srv.ClearCookie(rw, srv.cookieName, "/")
resp := rw.Result() resp := rw.Result()
@@ -36,7 +36,7 @@ func TestClearCookieWithDomain(t *testing.T) {
srv := spawnAnubis(t, Options{CookieDomain: "techaro.lol"}) srv := spawnAnubis(t, Options{CookieDomain: "techaro.lol"})
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
srv.ClearCookie(rw) srv.ClearCookie(rw, srv.cookieName, "/")
resp := rw.Result() resp := rw.Result()

View File

@@ -12,7 +12,6 @@ type Bot struct {
Challenge *config.ChallengeRules Challenge *config.ChallengeRules
Name string Name string
Action config.Rule Action config.Rule
Weight *config.Weight
} }
func (b Bot) Hash() string { func (b Bot) Hash() string {

View File

@@ -7,15 +7,12 @@ import (
) )
type CheckResult struct { type CheckResult struct {
Name string Name string
Rule config.Rule Rule config.Rule
Weight int
} }
func (cr CheckResult) LogValue() slog.Value { func (cr CheckResult) LogValue() slog.Value {
return slog.GroupValue( return slog.GroupValue(
slog.String("name", cr.Name), slog.String("name", cr.Name),
slog.String("rule", string(cr.Rule)), slog.String("rule", string(cr.Rule)))
slog.Int("weight", cr.Weight),
)
} }

View File

@@ -39,28 +39,20 @@ const (
RuleAllow Rule = "ALLOW" RuleAllow Rule = "ALLOW"
RuleDeny Rule = "DENY" RuleDeny Rule = "DENY"
RuleChallenge Rule = "CHALLENGE" RuleChallenge Rule = "CHALLENGE"
RuleWeigh Rule = "WEIGH"
RuleBenchmark Rule = "DEBUG_BENCHMARK" RuleBenchmark Rule = "DEBUG_BENCHMARK"
) )
type Algorithm string const DefaultAlgorithm = "fast"
const (
AlgorithmUnknown Algorithm = ""
AlgorithmFast Algorithm = "fast"
AlgorithmSlow Algorithm = "slow"
)
type BotConfig struct { type BotConfig struct {
UserAgentRegex *string `json:"user_agent_regex,omitempty"` UserAgentRegex *string `json:"user_agent_regex"`
PathRegex *string `json:"path_regex,omitempty"` PathRegex *string `json:"path_regex"`
HeadersRegex map[string]string `json:"headers_regex,omitempty"` HeadersRegex map[string]string `json:"headers_regex"`
Expression *ExpressionOrList `json:"expression,omitempty"` Expression *ExpressionOrList `json:"expression"`
Challenge *ChallengeRules `json:"challenge,omitempty"` Challenge *ChallengeRules `json:"challenge,omitempty"`
Weight *Weight `json:"weight,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Action Rule `json:"action"` Action Rule `json:"action"`
RemoteAddr []string `json:"remote_addresses,omitempty"` RemoteAddr []string `json:"remote_addresses"`
} }
func (b BotConfig) Zero() bool { func (b BotConfig) Zero() bool {
@@ -152,7 +144,7 @@ func (b BotConfig) Valid() error {
} }
switch b.Action { switch b.Action {
case RuleAllow, RuleBenchmark, RuleChallenge, RuleDeny, RuleWeigh: case RuleAllow, RuleBenchmark, RuleChallenge, RuleDeny:
// okay // okay
default: default:
errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action)) errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
@@ -164,10 +156,6 @@ func (b BotConfig) Valid() error {
} }
} }
if b.Action == RuleWeigh && b.Weight == nil {
b.Weight = &Weight{Adjust: 5}
}
if len(errs) != 0 { if len(errs) != 0 {
return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...)) return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...))
} }
@@ -176,15 +164,14 @@ func (b BotConfig) Valid() error {
} }
type ChallengeRules struct { type ChallengeRules struct {
Algorithm Algorithm `json:"algorithm"` Algorithm string `json:"algorithm"`
Difficulty int `json:"difficulty"` Difficulty int `json:"difficulty"`
ReportAs int `json:"report_as"` ReportAs int `json:"report_as"`
} }
var ( var (
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid") ErrChallengeDifficultyTooLow = errors.New("config.Bot.ChallengeRules: difficulty is too low (must be >= 1)")
ErrChallengeDifficultyTooLow = errors.New("config.Bot.ChallengeRules: difficulty is too low (must be >= 1)") ErrChallengeDifficultyTooHigh = errors.New("config.Bot.ChallengeRules: difficulty is too high (must be <= 64)")
ErrChallengeDifficultyTooHigh = errors.New("config.Bot.ChallengeRules: difficulty is too high (must be <= 64)")
) )
func (cr ChallengeRules) Valid() error { func (cr ChallengeRules) Valid() error {
@@ -198,13 +185,6 @@ func (cr ChallengeRules) Valid() error {
errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty)) errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty))
} }
switch cr.Algorithm {
case AlgorithmFast, AlgorithmSlow, AlgorithmUnknown:
// do nothing, it's all good
default:
errs = append(errs, fmt.Errorf("%w: %q", ErrChallengeRuleHasWrongAlgorithm, cr.Algorithm))
}
if len(errs) != 0 { if len(errs) != 0 {
return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...)) return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...))
} }
@@ -230,7 +210,7 @@ func (is *ImportStatement) open() (fs.File, error) {
func (is *ImportStatement) load() error { func (is *ImportStatement) load() error {
fin, err := is.open() fin, err := is.open()
if err != nil { if err != nil {
return fmt.Errorf("can't open %s: %w", is.Import, err) return fmt.Errorf("%w: %s: %w", ErrInvalidImportStatement, is.Import, err)
} }
defer fin.Close() defer fin.Close()

View File

@@ -130,20 +130,6 @@ func TestBotValid(t *testing.T) {
}, },
err: ErrChallengeDifficultyTooHigh, err: ErrChallengeDifficultyTooHigh,
}, },
{
name: "challenge wrong algorithm",
bot: BotConfig{
Name: "mozilla-ua",
Action: RuleChallenge,
PathRegex: p("Mozilla"),
Challenge: &ChallengeRules{
Difficulty: 420,
ReportAs: 4,
Algorithm: "high quality rips",
},
},
err: ErrChallengeRuleHasWrongAlgorithm,
},
{ {
name: "invalid cidr range", name: "invalid cidr range",
bot: BotConfig{ bot: BotConfig{
@@ -182,25 +168,6 @@ func TestBotValid(t *testing.T) {
}, },
err: nil, err: nil,
}, },
{
name: "weight rule without weight",
bot: BotConfig{
Name: "weight-adjust-if-mozilla",
Action: RuleWeigh,
UserAgentRegex: p("Mozilla"),
},
},
{
name: "weight rule with weight adjust",
bot: BotConfig{
Name: "weight-adjust-if-mozilla",
Action: RuleWeigh,
UserAgentRegex: p("Mozilla"),
Weight: &Weight{
Adjust: 5,
},
},
},
} }
for _, cs := range tests { for _, cs := range tests {
@@ -270,6 +237,7 @@ func TestImportStatement(t *testing.T) {
"bots", "bots",
"common", "common",
"crawlers", "crawlers",
"meta",
} { } {
if err := fs.WalkDir(data.BotPolicies, folderName, func(path string, d fs.DirEntry, err error) error { if err := fs.WalkDir(data.BotPolicies, folderName, func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
@@ -278,6 +246,9 @@ func TestImportStatement(t *testing.T) {
if d.IsDir() { if d.IsDir() {
return nil return nil
} }
if d.Name() == "README.md" {
return nil
}
tests = append(tests, testCase{ tests = append(tests, testCase{
name: "(data)/" + path, name: "(data)/" + path,
@@ -376,7 +347,7 @@ func TestBotConfigZero(t *testing.T) {
b.Challenge = &ChallengeRules{ b.Challenge = &ChallengeRules{
Difficulty: 4, Difficulty: 4,
ReportAs: 4, ReportAs: 4,
Algorithm: AlgorithmFast, Algorithm: DefaultAlgorithm,
} }
if b.Zero() { if b.Zero() {
t.Error("BotConfig with challenge rules is zero value") t.Error("BotConfig with challenge rules is zero value")

View File

@@ -14,8 +14,8 @@ var (
type ExpressionOrList struct { type ExpressionOrList struct {
Expression string `json:"-"` Expression string `json:"-"`
All []string `json:"all,omitempty"` All []string `json:"all"`
Any []string `json:"any,omitempty"` Any []string `json:"any"`
} }
func (eol ExpressionOrList) Equal(rhs *ExpressionOrList) bool { func (eol ExpressionOrList) Equal(rhs *ExpressionOrList) bool {
@@ -54,6 +54,9 @@ func (eol *ExpressionOrList) UnmarshalJSON(data []byte) error {
} }
func (eol *ExpressionOrList) Valid() error { func (eol *ExpressionOrList) Valid() error {
if eol.Expression == "" && len(eol.All) == 0 && len(eol.Any) == 0 {
return ErrExpressionEmpty
}
if len(eol.All) != 0 && len(eol.Any) != 0 { if len(eol.All) != 0 && len(eol.Any) != 0 {
return ErrExpressionCantHaveBoth return ErrExpressionCantHaveBoth
} }

View File

@@ -51,6 +51,13 @@ func TestExpressionOrListUnmarshal(t *testing.T) {
}`, }`,
validErr: ErrExpressionCantHaveBoth, validErr: ErrExpressionCantHaveBoth,
}, },
{
name: "expression-empty",
inp: `{
"any": []
}`,
validErr: ErrExpressionEmpty,
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var eol ExpressionOrList var eol ExpressionOrList

View File

@@ -1,7 +1,7 @@
{ {
"bots": [ "bots": [
{ {
"import": "(data)/bots/ai-robots-txt.yaml", "import": "(data)/bots/ai-catchall.yaml",
"name": "generic-browser", "name": "generic-browser",
"user_agent_regex": "Mozilla|Opera\n", "user_agent_regex": "Mozilla|Opera\n",
"action": "CHALLENGE" "action": "CHALLENGE"

View File

@@ -1,5 +1,5 @@
bots: bots:
- import: (data)/bots/ai-robots-txt.yaml - import: (data)/bots/ai-catchall.yaml
name: generic-browser name: generic-browser
user_agent_regex: > user_agent_regex: >
Mozilla|Opera Mozilla|Opera

View File

@@ -0,0 +1,8 @@
bots:
- name: total-randomness
action: ALLOW
expression:
all:
- '"Accept" in headers'
- headers["Accept"].contains("text/html")
- randInt(1) == 0

View File

@@ -1,6 +0,0 @@
bots:
- name: simple-weight-adjust
action: WEIGH
user_agent_regex: Mozilla
weight:
adjust: 5

View File

@@ -1,4 +0,0 @@
bots:
- name: weight
action: WEIGH
user_agent_regex: Mozilla

View File

@@ -1,5 +0,0 @@
package config
type Weight struct {
Adjust int `json:"adjust"`
}

View File

@@ -1,7 +1,11 @@
package expressions package expressions
import ( import (
"math/rand/v2"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext" "github.com/google/cel-go/ext"
) )
@@ -29,6 +33,20 @@ func NewEnvironment() (*cel.Env, error) {
cel.Variable("headers", cel.MapType(cel.StringType, cel.StringType)), cel.Variable("headers", cel.MapType(cel.StringType, cel.StringType)),
// Functions exposed to CEL programs: // Functions exposed to CEL programs:
cel.Function("randInt",
cel.Overload("randInt_int",
[]*cel.Type{cel.IntType},
cel.IntType,
cel.UnaryBinding(func(val ref.Val) ref.Val {
n, ok := val.(types.Int)
if !ok {
return types.ValOrErr(val, "value is not an integer, but is %T", val)
}
return types.Int(rand.IntN(int(n)))
}),
),
),
) )
} }

View File

@@ -1,47 +0,0 @@
package policy
import (
"fmt"
"net/http"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/store/valkey"
)
type PassRateChecker struct {
store *valkey.Store
header string
rate float64
}
func NewPassRateChecker(store *valkey.Store, rate float64) Checker {
return &PassRateChecker{
store: store,
rate: rate,
header: "User-Agent",
}
}
func (prc *PassRateChecker) Hash() string {
return internal.SHA256sum(fmt.Sprintf("pass rate checker::%s", prc.header))
}
func (prc *PassRateChecker) Check(r *http.Request) (bool, error) {
data, err := prc.store.MultiGetInt(r.Context(), [][]string{
{"pass_rate", prc.header, r.Header.Get(prc.header), "pass"},
{"pass_rate", prc.header, r.Header.Get(prc.header), "challenges_issued"},
{"pass_rate", prc.header, r.Header.Get(prc.header), "fail"},
})
if err != nil {
return false, err
}
passCount, challengeCount, failCount := data[0], data[1], data[2]
passRate := float64(passCount-failCount) / float64(challengeCount)
if passRate >= prc.rate {
return true, nil
}
return false, nil
}

View File

@@ -5,10 +5,9 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/TecharoHQ/anubis/lib/policy/config"
) )
var ( var (
@@ -16,6 +15,8 @@ var (
Name: "anubis_policy_results", Name: "anubis_policy_results",
Help: "The results of each policy rule", Help: "The results of each policy rule",
}, []string{"rule", "action"}) }, []string{"rule", "action"})
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
) )
type ParsedConfig struct { type ParsedConfig struct {
@@ -107,19 +108,15 @@ func ParseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedCon
parsedBot.Challenge = &config.ChallengeRules{ parsedBot.Challenge = &config.ChallengeRules{
Difficulty: defaultDifficulty, Difficulty: defaultDifficulty,
ReportAs: defaultDifficulty, ReportAs: defaultDifficulty,
Algorithm: config.AlgorithmFast, Algorithm: "fast",
} }
} else { } else {
parsedBot.Challenge = b.Challenge parsedBot.Challenge = b.Challenge
if parsedBot.Challenge.Algorithm == config.AlgorithmUnknown { if parsedBot.Challenge.Algorithm == "" {
parsedBot.Challenge.Algorithm = config.AlgorithmFast parsedBot.Challenge.Algorithm = config.DefaultAlgorithm
} }
} }
if b.Weight != nil {
parsedBot.Weight = b.Weight
}
parsedBot.Rules = cl parsedBot.Rules = cl
result.Bots = append(result.Bots, parsedBot) result.Bots = append(result.Bots, parsedBot)

View File

@@ -1,4 +1,8 @@
bots: bots:
- name: cloudflare-workers - name: cloudflare-workers
expression: '"Cf-Worker" in headers' expression: '"Cf-Worker" in headers'
action: DENY action: DENY
status_codes:
CHALLENGE: 401
DENY: 403

View File

@@ -2,4 +2,8 @@ bots:
- name: cloudflare-workers - name: cloudflare-workers
headers_regex: headers_regex:
CF-Worker: .* CF-Worker: .*
action: DENY action: DENY
status_codes:
CHALLENGE: 401
DENY: 403

9
lib/testdata/hack-test.json vendored Normal file
View File

@@ -0,0 +1,9 @@
[
{
"name": "ipv6-ula",
"action": "ALLOW",
"remote_addresses": [
"fc00::/7"
]
}
]

3
lib/testdata/hack-test.yaml vendored Normal file
View File

@@ -0,0 +1,3 @@
- name: well-known
path_regex: ^/.well-known/.*$
action: ALLOW

View File

@@ -0,0 +1,8 @@
bots:
- name: generic-bot-catchall
user_agent_regex: (?i:bot|crawler)
action: CHALLENGE
challenge:
difficulty: 16
report_as: 4
algorithm: hunter2 # invalid algorithm

12
lib/testdata/rule_change.yaml vendored Normal file
View File

@@ -0,0 +1,12 @@
bots:
- name: old-rule
path_regex: ^/old$
action: CHALLENGE
- name: new-rule
path_regex: ^/new$
action: CHALLENGE
status_codes:
CHALLENGE: 401
DENY: 403

246
lib/thr1/thr1.go Normal file
View File

@@ -0,0 +1,246 @@
package thr1
import (
"crypto/sha256"
"encoding/hex"
"log/slog"
"net/http"
"regexp"
"sort"
"strconv"
"strings"
)
func Fingerprint(r *http.Request) string {
result := strings.Join([]string{
thr1Head(r),
thr1Lang(r),
thr1Sec(r),
thr1UA(r),
thr1Encoding(r),
}, "_")
slog.Info("THR1 got", "method", r.Method, "path", r.URL.Path, "thr1", result)
return result
}
func thr1Head(r *http.Request) string {
method := strings.ToLower(r.Method)
if len(method) > 3 {
method = method[:3]
}
version := "00"
if override := r.Header.Get("X-Http-Version"); override != "" {
switch strings.TrimSpace(strings.ToUpper(override)) {
case "HTTP/1.0":
version = "10"
case "HTTP/1.1":
version = "11"
case "HTTP/2.0":
version = "20"
case "HTTP/3.0":
version = "30"
}
} else {
switch {
case r.ProtoMajor == 1 && r.ProtoMinor == 0:
version = "10"
case r.ProtoMajor == 1 && r.ProtoMinor == 1:
version = "11"
case r.ProtoMajor == 2:
version = "20"
case r.ProtoMajor == 3:
version = "30"
}
}
hasSec := false
for k := range r.Header {
if strings.HasPrefix(strings.ToLower(k), "sec-") {
hasSec = true
break
}
}
return method + version + strconv.FormatBool(hasSec)[:2]
}
func thr1Encoding(r *http.Request) string {
raw := r.Header.Get("Accept-Encoding")
if raw == "" {
return "none-00"
}
encodings := strings.Split(raw, ",")
count := len(encodings)
if count > 99 {
count = 99
}
seen := make(map[string]struct{})
var available []string
for _, e := range encodings {
enc := strings.ToLower(strings.TrimSpace(strings.Split(e, ";")[0]))
if enc != "" {
if _, exists := seen[enc]; !exists {
available = append(available, enc)
seen[enc] = struct{}{}
}
}
}
priorities := map[string]int{
"zstd": 1,
"br": 2,
"deflate": 3,
"gzip": 4,
"*": 5,
}
best := "none"
bestRank := 999 // arbitrarily high
for _, enc := range available {
if rank, ok := priorities[enc]; ok {
if rank < bestRank {
best = enc
bestRank = rank
}
}
}
if best == "*" {
best = "wild"
}
return best + "-" + pad2(count)
}
func pad2(n int) string {
if n < 10 {
return "0" + strconv.Itoa(n)
}
if n > 99 {
return "99"
}
return strconv.Itoa(n)
}
func thr1Lang(r *http.Request) string {
raw := r.Header.Get("Accept-Language")
if raw == "" {
return "-000000000"
}
trimmed := first4AlphaNum(strings.ToLower(raw)) + "-"
sum := sha256.Sum256([]byte(raw))
return trimmed + hex.EncodeToString(sum[:])[:9]
}
func first4AlphaNum(s string) string {
out := make([]rune, 0, 4)
for _, ch := range s {
if len(out) == 4 {
break
}
if ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') {
out = append(out, ch)
}
}
for len(out) < 4 {
out = append(out, '0')
}
return string(out)
}
func thr1Sec(r *http.Request) string {
var lines []string
for k, vs := range r.Header {
lkey := strings.ToLower(k)
if !strings.HasPrefix(lkey, "sec-") || lkey == "sec-fetch-user" {
continue
}
switch lkey {
case "sec-ch-ua":
lines = append(lines, parseSecChUA(vs))
case "sec-ch-ua-mobile":
lines = append(lines, parseSecCHSimple("mobile", vs))
case "sec-ch-ua-platform":
lines = append(lines, parseSecCHSimple("platform", vs))
case "sec-ch-ua-platform-version":
lines = append(lines, parseSecCHSimple("platform_version", vs))
case "sec-ch-ua-model":
lines = append(lines, parseSecCHSimple("model", vs))
case "sec-ch-ua-full-version":
lines = append(lines, parseSecCHSimple("full_version", vs))
default:
for _, v := range vs {
v = strings.Trim(v, `" `)
lines = append(lines, lkey+":"+v)
}
}
}
sort.Strings(lines)
canonical := strings.Join(lines, "\n")
sum := sha256.Sum256([]byte(canonical))
return "sec-" + hex.EncodeToString(sum[:])[:9]
}
var brandVersionRe = regexp.MustCompile(`\s*"([^"]+)";v="([^"]+)"`)
func parseSecChUA(vs []string) string {
type pair struct{ Brand, Version string }
var pairs []pair
for _, v := range vs {
for _, match := range brandVersionRe.FindAllStringSubmatch(v, -1) {
if len(match) != 3 {
continue
}
brand := match[1]
version := match[2]
if brand == "Not=A?Brand" {
continue
}
pairs = append(pairs, pair{brand, version})
}
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].Brand < pairs[j].Brand
})
var sb strings.Builder
sb.WriteString("ua:")
for i, p := range pairs {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p.Brand + "/" + p.Version)
}
return sb.String()
}
func parseSecCHSimple(key string, vs []string) string {
for _, v := range vs {
v = strings.Trim(v, `" `)
if key == "mobile" {
switch v {
case "?1":
return "mobile:true"
case "?0":
return "mobile:false"
default:
continue
}
}
return key + ":" + v
}
return key + ":"
}
func thr1UA(r *http.Request) string {
ua := r.Header.Get("User-Agent")
sum := sha256.Sum256([]byte(ua))
return hex.EncodeToString(sum[:])[:9]
}

212
package-lock.json generated
View File

@@ -1,17 +1,17 @@
{ {
"name": "@techaro/anubis", "name": "@techaro/anubis",
"version": "1.18.0", "version": "1.19.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@techaro/anubis", "name": "@techaro/anubis",
"version": "1.18.0", "version": "1.19.1",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"cssnano": "^7.0.7", "cssnano": "^7.0.7",
"cssnano-preset-advanced": "^7.0.7", "cssnano-preset-advanced": "^7.0.7",
"esbuild": "^0.25.4", "esbuild": "^0.25.5",
"playwright": "^1.52.0", "playwright": "^1.52.0",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
@@ -20,9 +20,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -37,9 +37,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -54,9 +54,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -71,9 +71,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -88,9 +88,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -105,9 +105,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -122,9 +122,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -139,9 +139,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -156,9 +156,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -173,9 +173,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -190,9 +190,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -207,9 +207,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -224,9 +224,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -241,9 +241,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -258,9 +258,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -275,9 +275,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -292,9 +292,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -309,9 +309,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -326,9 +326,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -343,9 +343,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -360,9 +360,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -377,9 +377,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -394,9 +394,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -411,9 +411,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -428,9 +428,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1045,9 +1045,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -1058,31 +1058,31 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4", "@esbuild/aix-ppc64": "0.25.5",
"@esbuild/android-arm": "0.25.4", "@esbuild/android-arm": "0.25.5",
"@esbuild/android-arm64": "0.25.4", "@esbuild/android-arm64": "0.25.5",
"@esbuild/android-x64": "0.25.4", "@esbuild/android-x64": "0.25.5",
"@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-arm64": "0.25.5",
"@esbuild/darwin-x64": "0.25.4", "@esbuild/darwin-x64": "0.25.5",
"@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.5",
"@esbuild/freebsd-x64": "0.25.4", "@esbuild/freebsd-x64": "0.25.5",
"@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm": "0.25.5",
"@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-arm64": "0.25.5",
"@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-ia32": "0.25.5",
"@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-loong64": "0.25.5",
"@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-mips64el": "0.25.5",
"@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-ppc64": "0.25.5",
"@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-riscv64": "0.25.5",
"@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-s390x": "0.25.5",
"@esbuild/linux-x64": "0.25.4", "@esbuild/linux-x64": "0.25.5",
"@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.5",
"@esbuild/netbsd-x64": "0.25.4", "@esbuild/netbsd-x64": "0.25.5",
"@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.5",
"@esbuild/openbsd-x64": "0.25.4", "@esbuild/openbsd-x64": "0.25.5",
"@esbuild/sunos-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.5",
"@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-arm64": "0.25.5",
"@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-ia32": "0.25.5",
"@esbuild/win32-x64": "0.25.4" "@esbuild/win32-x64": "0.25.5"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@techaro/anubis", "name": "@techaro/anubis",
"version": "1.18.0", "version": "1.19.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -10,7 +10,7 @@
"test:integration:docker": "npm run assets && go test -v ./internal/test --playwright-runner=docker", "test:integration:docker": "npm run assets && go test -v ./internal/test --playwright-runner=docker",
"assets": "go generate ./... && ./web/build.sh && ./xess/build.sh", "assets": "go generate ./... && ./web/build.sh && ./xess/build.sh",
"build": "npm run assets && go build -o ./var/anubis ./cmd/anubis", "build": "npm run assets && go build -o ./var/anubis ./cmd/anubis",
"dev": "npm run assets && go run ./cmd/anubis --use-remote-address", "dev": "npm run assets && go run ./cmd/anubis --use-remote-address --target http://localhost:3000",
"container": "npm run assets && go run ./cmd/containerbuild", "container": "npm run assets && go run ./cmd/containerbuild",
"package": "yeet", "package": "yeet",
"lint": "make lint" "lint": "make lint"
@@ -20,7 +20,7 @@
"devDependencies": { "devDependencies": {
"cssnano": "^7.0.7", "cssnano": "^7.0.7",
"cssnano-preset-advanced": "^7.0.7", "cssnano-preset-advanced": "^7.0.7",
"esbuild": "^0.25.4", "esbuild": "^0.25.5",
"playwright": "^1.52.0", "playwright": "^1.52.0",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",

View File

@@ -12,6 +12,8 @@ CacheDirectory=anubis/%i
CacheDirectoryMode=0755 CacheDirectoryMode=0755
StateDirectory=anubis/%i StateDirectory=anubis/%i
StateDirectoryMode=0755 StateDirectoryMode=0755
RuntimeDirectory=anubis
RuntimeDirectoryMode=0755
ReadWritePaths=/run ReadWritePaths=/run
[Install] [Install]

24
run/openrc/anubis.confd Normal file
View File

@@ -0,0 +1,24 @@
# The URL of the service that Anubis should forward valid requests to. Supports
# Unix domain sockets.
#ANUBIS_TARGET="http://localhost:3923"
#ANUBIS_TARGET="unix:///path/to/socket"
# The network address that Anubis listens on.
#
# If unset, listen on /run/anubis_${instance}/anubis.sock Unix socket instead.
#ANUBIS_BIND_PORT=":8923"
# The network address that Anubis serves Prometheus metrics on.
#
# If unset, listen on /run/anubis_${instance}/metrix.sock Unix socket instead.
#ANUBIS_METRICS_BIND_PORT=":9090"
# The difficulty of the challenge, or the number of leading zeroes that must be
# in successful responses.
#ANUBIS_DIFFICULTY=4
# Additional command-line options for Anubis.
#ANUBIS_OPTS=""
# Configure the user[:group] Anubis will run as.
#command_user="anubis:anubis"

34
run/openrc/anubis.initd Normal file
View File

@@ -0,0 +1,34 @@
#!/sbin/openrc-run
# shellcheck shell=sh
instance=${RC_SVCNAME#*.}
description="Anubis HTTP defense proxy (instance ${instance})"
supervisor="supervise-daemon"
command="/usr/bin/anubis"
command_args="\
-bind ${ANUBIS_BIND_PORT:-/run/anubis_${instance?}/anubis.sock -bind-network unix} \
-metrics-bind ${ANUBIS_METRICS_BIND_PORT:-/run/anubis_${instance?}/metrics.sock -metrics-bind-network unix} \
-target ${ANUBIS_TARGET:-http://localhost:3923} \
-difficulty ${ANUBIS_DIFFICULTY:-4} \
${ANUBIS_OPTS}
"
command_background=1
pidfile="/run/anubis_${instance?}/anubis.pid"
: "${command_user:=anubis:anubis}"
depend() {
use net firewall
}
start_pre() {
if [ "${instance?}" = "${RC_SVCNAME?}" ]; then
eerror "${RC_SVCNAME?} cannot be started directly. You must create"
eerror "symbolic links to it for the services you want to start"
eerror "and add those to the appropriate runlevels."
return 1
fi
checkpath -d -o "${command_user?}" "/run/anubis_${instance?}"
}

View File

@@ -42,11 +42,9 @@ templ base(title string, body templ.Component, challenge any, ogTags map[string]
border-radius: 1rem; border-radius: 1rem;
overflow: hidden; overflow: hidden;
margin: 1rem 0 2rem; margin: 1rem 0 2rem;
outline-color: #b16286; outline-offset: 2px;
outline-offset: 2px; outline: #b16286 solid 4px;
outline-style: solid; }
outline-width: 4px;
}
.bar-inner { .bar-inner {
background-color: #b16286; background-color: #b16286;
@@ -123,6 +121,7 @@ templ index() {
>JShelter</a> will disable. Please disable JShelter or other such >JShelter</a> will disable. Please disable JShelter or other such
plugins for this domain. plugins for this domain.
</p> </p>
<p>This website is running Anubis version <code>{ anubis.Version }</code>.</p>
</details> </details>
<noscript> <noscript>
<p> <p>
@@ -154,15 +153,15 @@ templ errorPage(message string, mail string) {
} }
templ StaticHappy() { templ StaticHappy() {
<div class="centered-div"> <div class="centered-div">
<img <img
style="display:none;" style="display:none;"
style="width:100%;max-width:256px;" style="width:100%;max-width:256px;"
src={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + src={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
anubis.Version } anubis.Version }
/> />
<p>This is just a check endpoint for your reverse proxy to use.</p> <p>This is just a check endpoint for your reverse proxy to use.</p>
</div> </div>
} }
templ bench() { templ bench() {

Some files were not shown because too many files have changed in this diff Show More