Compare commits

..

46 Commits

Author SHA1 Message Date
Benjamin Bouvier f21706eb12 feat(data): add Meta's web indexer used for AI purposes (#1573)
This indexer is documented in
https://developers.facebook.com/docs/sharing/webmasters/web-crawlers. I
saw it parsing the entirety of my Forgejo instance, so I suggest to
widely block it.

Signed-off-by: Benjamin Bouvier <benjamin@bouvier.cc>
2026-04-21 16:56:23 -04:00
Xe Iaso d5ccf9c670 feat: move metrics server config to the policy file (#1572)
* feat(config): add metrics bind config to policy file with flag hack

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

* feat(internal): move SetupListener from main

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

* fix(main): use internal.SetupListener

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

* fix(config): add metrics socket mode

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

* feat: move metrics server to a dedicated package

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

* doc: add metrics server configuration docs

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

* doc(default-config): add vague references to metrics server

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>
2026-04-21 15:36:11 -04:00
Xe Iaso 3a6e368179 Revert "ci(ssh): enable local machines again"
This reverts commit 98afcf8c64.
2026-04-18 11:30:32 -04:00
Xe Iaso 98afcf8c64 ci(ssh): enable local machines again
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-18 11:22:04 -04:00
dependabot[bot] 982394ca91 build(deps): bump the github-actions group across 1 directory with 7 updates (#1560)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2026-04-14 03:35:30 +00:00
dependabot[bot] 66b7b27aef build(deps): bump the npm group across 1 directory with 5 updates (#1559)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2026-04-13 23:31:39 -04:00
Xe Iaso cc07be8a9e chore: spelling
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-10 21:40:03 -04:00
Xe Iaso 04f8b6b148 chore(sponsors): add qwertiko and wenet
Closes: #1553
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-10 21:36:40 -04:00
Xe Iaso dbd64e0f4f docs(faq): document minimum versions of browsers Anubis supports (#1540)
* docs(faq): document minimum versions of browsers Anubis supports

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

* chore: update spelling

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-24 15:59:12 +00:00
dependabot[bot] 3acf8ee387 build(deps): bump the npm group with 5 updates (#1531)
Bumps the npm group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [preact](https://github.com/preactjs/preact) | `10.28.4` | `10.29.0` |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.4.3` | `20.5.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.4.3` | `20.5.0` |
| [baseline-browser-mapping](https://github.com/web-platform-dx/baseline-browser-mapping) | `2.10.0` | `2.10.8` |
| [esbuild](https://github.com/evanw/esbuild) | `0.27.3` | `0.27.4` |


Updates `preact` from 10.28.4 to 10.29.0
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.28.4...10.29.0)

Updates `@commitlint/cli` from 20.4.3 to 20.5.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.5.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.4.3 to 20.5.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.5.0/@commitlint/config-conventional)

Updates `baseline-browser-mapping` from 2.10.0 to 2.10.8
- [Release notes](https://github.com/web-platform-dx/baseline-browser-mapping/releases)
- [Commits](https://github.com/web-platform-dx/baseline-browser-mapping/compare/v2.10.0...v2.10.8)

Updates `esbuild` from 0.27.3 to 0.27.4
- [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.27.3...v0.27.4)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: "@commitlint/cli"
  dependency-version: 20.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: baseline-browser-mapping
  dependency-version: 2.10.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.27.4
  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>
2026-03-24 15:54:04 +00:00
Xe Iaso 57260a562a chore(sponsors): add embeDD GmbH (#1539)
* chore(sponsors): add embeDD GmbH

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

* chore: update spelling

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-24 15:51:32 +00:00
dependabot[bot] e42a328843 build(deps): bump actions-hub/kubectl in the github-actions group (#1532)
Bumps the github-actions group with 1 update: [actions-hub/kubectl](https://github.com/actions-hub/kubectl).


Updates `actions-hub/kubectl` from 1.35.2 to 1.35.3
- [Release notes](https://github.com/actions-hub/kubectl/releases)
- [Commits](https://github.com/actions-hub/kubectl/compare/5ada4e2c02eacc03978c2437e95c8b0f979a9619...934aaa4354bbbc3d2176ae8d7cae92d515032dff)

---
updated-dependencies:
- dependency-name: actions-hub/kubectl
  dependency-version: 1.35.3
  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>
2026-03-23 15:33:43 +00:00
Michael c4b26e5a75 l10n(de): revise and update German translations (#1457)
* l10n(de): revise and update German translations

Improve strings for clarity and naturalness, update Wikipedia link to German version.

Signed-off-by: Michael <87752300+michi-onl@users.noreply.github.com>

* test(localization): fix tests

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

---------

Signed-off-by: Michael <87752300+michi-onl@users.noreply.github.com>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2026-03-21 20:02:49 +00:00
Duy 1342539a41 chore: update vi.json (#1459)
* Update vi.json

Signed-off-by: Duy <104808921+honhatduy@users.noreply.github.com>

* test(localization): fix tests

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

---------

Signed-off-by: Duy <104808921+honhatduy@users.noreply.github.com>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2026-03-21 20:01:21 +00:00
ramromcode bd6f654e1f feat(i18n): add Bulgarian language translation (#1394)
* feat(localization): add bulgarian language translation

* fix(locales/bg): make tests pass

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

---------

Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <xe.iaso@techaro.lol>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2026-03-21 19:56:27 +00:00
Xe Iaso d2c3a326af feat(anubis): enable pprof endpoints on the metrics server (#1530)
Closes: #1366

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-21 19:43:47 +00:00
dwhitemv25 c2ed62f51d fix: Address issued raised in #1193 (#1519)
* Resolve #1193

Address documentation and error message issues around REDIRECT_DOMAINS and required keywords in bot specifications.

* Add CHANGELOG entry
2026-03-21 19:32:25 +00:00
Jason Cameron 11c4adc6b4 fix: add cel iterator (#1465)
* fix: enable CEL iterators

Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>

* test: add unit tests for CELChecker map iteration

Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>

* fix: implement map iterators for HTTPHeaders and URLValues to resolve CEL internal errors

Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>

* fix: replace checker.NewMapIterator with newMapIterator for HTTPHeaders and URLValues

Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>

---------

Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>
2026-03-21 19:30:05 +00:00
BALLOON | FU-SEN edbfd180b8 locales/ja: Change the position of the バージョン (version) (#1527)
When displayed in Japanese, the `バージョン` (version) is in the middle, but the version number is at the end, so it is displayed strangely. Improve this.

**"version_info":**
```
このウェブサイトはAnubisバージョンで動作しています
```
to
```
このウェブサイトはAnubisで動作しています バージョン
```

Signed-off-by: BALLOON | FU-SEN <5434159+fu-sen@users.noreply.github.com>
2026-03-21 06:36:40 +00:00
Xe Iaso efde4f0dc7 docs(faq): document that disabling JIT makes Anubis slow (#1526)
* docs(faq): document that disabling JIT makes Anubis slow

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

* chore: fix spelling

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>
2026-03-20 22:16:50 +00:00
Marielle Volz 24857f430f feat(data): add Citoid to good bots list (#1524)
* Add Wikimedia Foundation citoid services file

Wikimedia Foundation runs a service called citoid which retrieves citation metadata from urls in order to create formatted citations. 

This file contains the ip ranges allocated to the WMF (https://wikitech.wikimedia.org/wiki/IP_and_AS_allocations) from which the services make requests, as well as regex for the User-Agents from both services used to generate citations (citoid, and Zotero's translation-server which citoid makes requests to as well in order to generate the metadata).

Signed-off-by: Marielle Volz <marielle.volz@gmail.com>

* Add Wikimedia Citoid crawler to allowed list

Signed-off-by: Marielle Volz <marielle.volz@gmail.com>

* chore: update spelling

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

---------

Signed-off-by: Marielle Volz <marielle.volz@gmail.com>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2026-03-20 11:13:26 +00:00
Simon Rozman e0ece7d333 feat(docs): Update HAProxy Advanced Variant documentation (#1521)
Added note on HAProxy's responsibility to handle Git HTTP and bot
traffic whitelisting.

Signed-off-by: Simon Rozman <simon@rozman.si>
2026-03-19 11:03:14 +00:00
fhoekstra 3eab1d873d (docs): Add instructions on using Anubis with envoy-gateway (#1460)
Signed-off-by: fhoekstra <32362869+fhoekstra@users.noreply.github.com>
2026-03-18 18:03:29 +00:00
Jason Cameron c7b31d0ca9 fix: nil ptr deref (#1467)
Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>
Signed-off-by: Jason Cameron <git@jasoncameron.dev>
2026-03-18 18:02:57 +00:00
Xe Iaso 3154ff5004 chore: add sponsor logo
Closes: #1472
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-18 16:07:06 +00:00
Jason Cameron 5186d7d3ad chore: gofix (#1466)
Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>
2026-03-18 14:17:28 +00:00
Xe Iaso c6d968874d chore: update spelling
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-18 14:15:35 +00:00
Xe Iaso 14a8d0c75e chore: add uvensys logo
Closes: #1517
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-18 14:14:48 +00:00
Xe Iaso 0ea13dcee2 ci(ssh): disable homelab jobs because it's offline and i'm halfway across the world, oh well
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-17 14:54:04 +00:00
dependabot[bot] a2a4cdebd6 build(deps): bump the npm group across 1 directory with 6 updates (#1512)
Bumps the npm group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [preact](https://github.com/preactjs/preact) | `10.28.3` | `10.28.4` |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.4.1` | `20.4.3` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.4.1` | `20.4.3` |
| [baseline-browser-mapping](https://github.com/web-platform-dx/baseline-browser-mapping) | `2.9.19` | `2.10.0` |
| [cssnano](https://github.com/cssnano/cssnano) | `7.1.2` | `7.1.3` |
| [cssnano-preset-advanced](https://github.com/cssnano/cssnano) | `7.0.10` | `7.0.11` |



Updates `preact` from 10.28.3 to 10.28.4
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.28.3...10.28.4)

Updates `@commitlint/cli` from 20.4.1 to 20.4.3
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.4.3/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.4.1 to 20.4.3
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.4.3/@commitlint/config-conventional)

Updates `baseline-browser-mapping` from 2.9.19 to 2.10.0
- [Release notes](https://github.com/web-platform-dx/baseline-browser-mapping/releases)
- [Commits](https://github.com/web-platform-dx/baseline-browser-mapping/compare/v2.9.19...v2.10.0)

Updates `cssnano` from 7.1.2 to 7.1.3
- [Release notes](https://github.com/cssnano/cssnano/releases)
- [Commits](https://github.com/cssnano/cssnano/compare/cssnano@7.1.2...cssnano@7.1.3)

Updates `cssnano-preset-advanced` from 7.0.10 to 7.0.11
- [Release notes](https://github.com/cssnano/cssnano/releases)
- [Commits](https://github.com/cssnano/cssnano/compare/cssnano-preset-advanced@7.0.10...cssnano-preset-advanced@7.0.11)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.28.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@commitlint/cli"
  dependency-version: 20.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: baseline-browser-mapping
  dependency-version: 2.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: cssnano
  dependency-version: 7.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: cssnano-preset-advanced
  dependency-version: 7.0.11
  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>
2026-03-16 10:48:22 +00:00
dependabot[bot] 168fe79802 build(deps): bump the github-actions group across 1 directory with 11 updates (#1516)
Bumps the github-actions group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.2.0` | `6.3.0` |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.2.0` | `6.3.0` |
| [docker/metadata-action](https://github.com/docker/metadata-action) | `5.10.0` | `6.0.0` |
| [docker/login-action](https://github.com/docker/login-action) | `3.7.0` | `4.0.0` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `3.2.0` | `4.1.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.12.0` | `4.0.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `6.18.0` | `7.0.0` |
| [actions-hub/kubectl](https://github.com/actions-hub/kubectl) | `1.35.1` | `1.35.2` |
| [dominikh/staticcheck-action](https://github.com/dominikh/staticcheck-action) | `1.4.0` | `1.4.1` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `6.0.0` | `7.0.0` |
| [shimataro/ssh-key-action](https://github.com/shimataro/ssh-key-action) | `2.7.0` | `2.8.0` |



Updates `actions/setup-node` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/6044e13b5dc448c55e2357c09f80417699197238...53b83947a5a98c8d113130e565377fae1a50d02f)

Updates `actions/setup-go` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5...4b73464bb391d4059bd26b0524d20df3927bd417)

Updates `docker/metadata-action` from 5.10.0 to 6.0.0
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/c299e40c65443455700f0fdfc63efafe5b349051...030e881283bb7a6894de51c315a6bfe6a94e05cf)

Updates `docker/login-action` from 3.7.0 to 4.0.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...b45d80f862d83dbcd57f89517bcf500b2ab88fb2)

Updates `actions/attest-build-provenance` from 3.2.0 to 4.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/96278af6caaf10aea03fd8d33a09a777ca52d62f...a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32)

Updates `docker/setup-buildx-action` from 3.12.0 to 4.0.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f...4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd)

Updates `docker/build-push-action` from 6.18.0 to 7.0.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/263435318d21b8e681c14492fe198d362a7d2c83...d08e5c354a6adb9ed34480a06d141179aa583294)

Updates `actions-hub/kubectl` from 1.35.1 to 1.35.2
- [Release notes](https://github.com/actions-hub/kubectl/releases)
- [Commits](https://github.com/actions-hub/kubectl/compare/3ece3793e7a9fe94effe257d03ac834c815ea87d...5ada4e2c02eacc03978c2437e95c8b0f979a9619)

Updates `dominikh/staticcheck-action` from 1.4.0 to 1.4.1
- [Release notes](https://github.com/dominikh/staticcheck-action/releases)
- [Changelog](https://github.com/dominikh/staticcheck-action/blob/master/CHANGES.md)
- [Commits](https://github.com/dominikh/staticcheck-action/compare/024238d2898c874f26d723e7d0ff4308c35589a2...9716614d4101e79b4340dd97b10e54d68234e431)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

Updates `shimataro/ssh-key-action` from 2.7.0 to 2.8.0
- [Release notes](https://github.com/shimataro/ssh-key-action/releases)
- [Changelog](https://github.com/shimataro/ssh-key-action/blob/v2/CHANGELOG.md)
- [Commits](https://github.com/shimataro/ssh-key-action/compare/d4fffb50872869abe2d9a9098a6d9c5aa7d16be4...6b84f2e793b32fa0b03a379cadadec75cc539391)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/setup-go
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/metadata-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions-hub/kubectl
  dependency-version: 1.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: dominikh/staticcheck-action
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: shimataro/ssh-key-action
  dependency-version: 2.8.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>
2026-03-16 06:44:18 -04:00
Max Chernoff 865ba0983e docs: remove developer/code-quality (#1475)
PR #1451 added `CONTRIBUTING.md`, but the commit message guidelines
there conflict with the ones in `developer/code-quality.md`. Since
`CONTRIBUTING.md` is newer, presumably the guidelines there are what's
expected from new commits. But after removing that section from
`code-quality.md`, there's not much content left, so this commit just
deletes the file entirely.

Signed-off-by: Max Chernoff <git@maxchernoff.ca>
2026-03-16 06:43:31 -04:00
Léane GRASSER 27c994d3ce chore(l10n): update French translation (#1496)
Replaces translations from MT engines or AI with better, human-made ones
:)

Signed-off-by: Léane GRASSER <leane.grasser@proton.me>
2026-03-16 06:43:05 -04:00
p0008874 22412d0e22 docs(known-instances): Add missing one. (#1500)
* docs(known-instances): Add missing one.

Dolphin Emulator, FFmpeg, and Valve's official wiki.

Signed-off-by: p0008874 <75534590+p0008874@users.noreply.github.com>

* Update known-instances.md

Signed-off-by: p0008874 <75534590+p0008874@users.noreply.github.com>

---------

Signed-off-by: p0008874 <75534590+p0008874@users.noreply.github.com>
2026-03-16 06:42:25 -04:00
Xe Iaso c5ff5f0f26 chore: spelling
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-16 10:42:06 +00:00
Max Chernoff 78fe07a78f feat(http): set "Cache-Control: no-store" on error responses (#1474)
* refactor(http): split long line in respondWithStatus

Signed-off-by: Max Chernoff <git@maxchernoff.ca>

* feat(http): set `Cache-Control: no-store` on error responses

Since #132, Anubis has set `Cache-Control: no-store` on challenge
responses. However, this does not apply to deny responses, meaning that
if Anubis is configured to block certain user agents and is behind a
caching reverse proxy, this error page will be cached and served to all
subsequent requests, even those with an allowed user agent. This commit
configures the error page responder to also set the `Cache-Control`
header, meaning that deny and challenge responses will now both have the
same behaviour.

Signed-off-by: Max Chernoff <git@maxchernoff.ca>

* chore(spelling): add new words to allowlist

Signed-off-by: Max Chernoff <git@maxchernoff.ca>

* chore(actions): bump Go version to fix govulncheck errors

Signed-off-by: Max Chernoff <git@maxchernoff.ca>

---------

Signed-off-by: Max Chernoff <git@maxchernoff.ca>
Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>
Co-authored-by: Xe Iaso <xe.iaso@techaro.lol>
2026-03-16 10:36:40 +00:00
Xe Iaso 04fef9e033 ci: purge govulncheck, it's less signal than i hoped (#1515)
* ci: purge govulncheck, it's less signal than i hoped

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

* ci(go): use go stable

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

* ci: use go stable

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-03-16 10:30:43 +00:00
Mozi fa518e1b8c docs: fix mixed tab/space indentation in Caddy config example (#1506)
Assisted-by: Claude Opus 4.6 via Copilot

Signed-off-by: Mozi <29089388+pzhlkj6612@users.noreply.github.com>
2026-03-12 16:35:53 +00:00
Xe Iaso f38210fd84 docs(admin/policy): document ReadWritePaths for logging to files (#1469)
The default Anubis systemd configuration is very restrictive in
order to prevent any possible compromise of Anubis to be useful
by threat actors. As such, it assumes all logs will be pushed to
the system journal. Some administrators do not want Anubis' logs
to be pushed to the system journal and want Anubis to log to a
file instead.

This change documents how to set up ReadWritePaths in the Anubis
systemd configuration such that Anubis can lot to a file as
administrators expect.

Closes: #1468

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-02-19 12:24:34 +00:00
Xe Iaso 35b5e78a0d chore: tag v1.25.0
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-02-18 15:56:28 +00:00
Martin 4e0df8c643 feat(docs): Add HAProxy Configurations to Docs (#1424)
* Add HAProxy docs

* Add changes to Changelog

* Add CodeBlock import to haproxy.mdc

* Fix typos

* Add exceptions to spelling
2026-02-15 10:32:32 -05:00
dependabot[bot] c34ec67777 build(deps): bump the npm group across 1 directory with 2 updates (#1452)
Bumps the npm group with 2 updates in the / directory: [preact](https://github.com/preactjs/preact) and [esbuild](https://github.com/evanw/esbuild).


Updates `preact` from 10.28.2 to 10.28.3
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.28.2...10.28.3)

Updates `esbuild` from 0.27.2 to 0.27.3
- [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.27.2...v0.27.3)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.28.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.27.3
  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>
2026-02-15 10:32:07 -05:00
dependabot[bot] 61026976ec build(deps): bump the github-actions group across 1 directory with 6 updates (#1453)
Bumps the github-actions group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [docker/login-action](https://github.com/docker/login-action) | `3.6.0` | `3.7.0` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `3.1.0` | `3.2.0` |
| [actions-hub/kubectl](https://github.com/actions-hub/kubectl) | `1.35.0` | `1.35.1` |
| [actions/cache](https://github.com/actions/cache) | `5.0.2` | `5.0.3` |
| [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) | `5.5.3` | `6.1.1` |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `7.2.0` | `7.3.0` |



Updates `docker/login-action` from 3.6.0 to 3.7.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/5e57cd118135c172c3672efd75eb46360885c0ef...c94ce9fb468520275223c153574b00df6fe4bcc9)

Updates `actions/attest-build-provenance` from 3.1.0 to 3.2.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8...96278af6caaf10aea03fd8d33a09a777ca52d62f)

Updates `actions-hub/kubectl` from 1.35.0 to 1.35.1
- [Release notes](https://github.com/actions-hub/kubectl/releases)
- [Commits](https://github.com/actions-hub/kubectl/compare/f6d776bd78f4523e36d6c74d34f9941c242b2213...3ece3793e7a9fe94effe257d03ac834c815ea87d)

Updates `actions/cache` from 5.0.2 to 5.0.3
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/8b402f58fbc84540c8b491a91e594a4576fec3d7...cdf6c1fa76f9f475f3d7449005a359c84ca0f306)

Updates `amannn/action-semantic-pull-request` from 5.5.3 to 6.1.1
- [Release notes](https://github.com/amannn/action-semantic-pull-request/releases)
- [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/action-semantic-pull-request/compare/0723387faaf9b38adef4775cd42cfd5155ed6017...48f256284bd46cdaab1048c3721360e808335d50)

Updates `astral-sh/setup-uv` from 7.2.0 to 7.3.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/61cb8a9741eeb8a550a1b8544337180c0fc8476b...eac588ad8def6316056a12d4907a9d4d84ff7a3b)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/attest-build-provenance
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions-hub/kubectl
  dependency-version: 1.35.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/cache
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: amannn/action-semantic-pull-request
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.3.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>
2026-02-15 10:31:51 -05:00
Xe Iaso 189c5c021c chore: sync logo submissions (#1455)
* chore: sync logo submissions

Closes: #1447
Closes: #1438
Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: update spelling

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-02-15 15:29:32 +00:00
Martin dde186150b feat(docs): Add ANEXIA Sponsor logo (#1409)
* Add ANEXIA Sponsor logo

* Add changes to CHANGELOG.md

* Add missing words to spelling expect.txt

---------

Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>
Co-authored-by: Xe Iaso <xe.iaso@techaro.lol>
2026-02-15 15:21:44 +00:00
Xe Iaso a98f721957 docs: add AI coding tools policy (#1454)
* docs: add AI coding tools policy

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

* chore: remove symlinks

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

* docs(AGENTS): make compatible with opencode

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

* chore: update spelling

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-02-15 15:08:59 +00:00
131 changed files with 2216 additions and 832 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
<!-- <!--
delete me and describe your change here, give enough context for a maintainer to understand what and why delete me and describe your change here, give enough context for a maintainer to understand what and why
See https://anubis.techaro.lol/docs/developer/code-quality for more information See https://github.com/TecharoHQ/anubis/blob/main/CONTRIBUTING.md for more information
--> -->
Checklist: Checklist:
- [ ] Added a description of the changes to the `[Unreleased]` section of docs/docs/CHANGELOG.md - [ ] Added a description of the changes to the `[Unreleased]` section of docs/docs/CHANGELOG.md
- [ ] Added test cases to [the relevant parts of the codebase](https://anubis.techaro.lol/docs/developer/code-quality) - [ ] Added test cases to [the relevant parts of the codebase](https://github.com/TecharoHQ/anubis/blob/main/CONTRIBUTING.md)
- [ ] Ran integration tests `npm run test:integration` (unsupported on Windows, please use WSL) - [ ] Ran integration tests `npm run test:integration` (unsupported on Windows, please use WSL)
- [ ] All of my commits have [verified signatures](https://anubis.techaro.lol/docs/developer/signed-commits) - [ ] All of my commits have [verified signatures](https://anubis.techaro.lol/docs/developer/signed-commits)
+14
View File
@@ -24,3 +24,17 @@ iplist
NArg NArg
blocklists blocklists
rififi rififi
prolocation
Prolocation
Necron
Stargate
FFXIV
uvensys
de
resourced
envoyproxy
unipromos
Samsung
wenet
qwertiko
setuplistener
+10 -1
View File
@@ -7,6 +7,7 @@ Aibrew
alibaba alibaba
alrest alrest
amazonbot amazonbot
anexia
anthro anthro
anubis anubis
anubistest anubistest
@@ -64,6 +65,7 @@ cidranger
ckie ckie
CLAUDE CLAUDE
cloudflare cloudflare
cloudsolutions
Codespaces Codespaces
confd confd
containerbuild containerbuild
@@ -78,6 +80,7 @@ databento
dayjob dayjob
dco dco
DDOS DDOS
ddwrt
Debian Debian
debrpm debrpm
decaymap decaymap
@@ -100,6 +103,7 @@ duckduckbot
eerror eerror
ellenjoe ellenjoe
emacs emacs
embe
enbyware enbyware
etld etld
everyones everyones
@@ -117,6 +121,7 @@ FCr
fcrdns fcrdns
fediverse fediverse
ffprobe ffprobe
fhdr
financials financials
finfos finfos
Firecrawl Firecrawl
@@ -154,6 +159,7 @@ grw
gzw gzw
Hashcash Hashcash
hashrate hashrate
hdr
headermap headermap
healthcheck healthcheck
healthz healthz
@@ -163,6 +169,7 @@ Hetzner
hmc hmc
homelab homelab
hostable hostable
HSTS
htmlc htmlc
htmx htmx
httpdebug httpdebug
@@ -215,7 +222,6 @@ LLU
loadbalancer loadbalancer
lol lol
lominsa lominsa
maintainership
malware malware
mcr mcr
memes memes
@@ -324,11 +330,13 @@ Spambot
spammer spammer
sparkline sparkline
spyderbot spyderbot
srcip
srv srv
stackoverflow stackoverflow
startprecmd startprecmd
stoppostcmd stoppostcmd
storetest storetest
strcmp
subgrid subgrid
subr subr
subrequest subrequest
@@ -353,6 +361,7 @@ Timpibot
TLog TLog
traefik traefik
trunc trunc
txn
uberspace uberspace
Unbreak Unbreak
unbreakdocker unbreakdocker
+3 -3
View File
@@ -22,12 +22,12 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: "24.11.0" node-version: "24.11.0"
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "1.25.7"
- name: install node deps - name: install node deps
run: | run: |
+4 -4
View File
@@ -26,18 +26,18 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: "24.11.0" node-version: "24.11.0"
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9 - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with: with:
images: ghcr.io/${{ github.repository }} images: ghcr.io/${{ github.repository }}
+6 -6
View File
@@ -36,17 +36,17 @@ jobs:
run: | run: |
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: "24.11.0" node-version: "24.11.0"
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9 - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- name: Log into registry - name: Log into registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -54,7 +54,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with: with:
images: ${{ env.IMAGE }} images: ${{ env.IMAGE }}
@@ -68,7 +68,7 @@ jobs:
SLOG_LEVEL: debug SLOG_LEVEL: debug
- name: Generate artifact attestation - name: Generate artifact attestation
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with: with:
subject-name: ${{ env.IMAGE }} subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }} subject-digest: ${{ steps.build.outputs.digest }}
+6 -6
View File
@@ -22,10 +22,10 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log into registry - name: Log into registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: techarohq username: techarohq
@@ -33,7 +33,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with: with:
images: ghcr.io/techarohq/anubis/docs images: ghcr.io/techarohq/anubis/docs
tags: | tags: |
@@ -42,7 +42,7 @@ jobs:
- name: Build and push - name: Build and push
id: build id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with: with:
context: ./docs context: ./docs
cache-to: type=gha cache-to: type=gha
@@ -53,14 +53,14 @@ jobs:
push: true push: true
- name: Apply k8s manifests to limsa lominsa - name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@f6d776bd78f4523e36d6c74d34f9941c242b2213 # v1.35.0 uses: actions-hub/kubectl@934aaa4354bbbc3d2176ae8d7cae92d515032dff # v1.35.3
env: env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }} KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with: with:
args: apply -k docs/manifest args: apply -k docs/manifest
- name: Apply k8s manifests to limsa lominsa - name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@f6d776bd78f4523e36d6c74d34f9941c242b2213 # v1.35.0 uses: actions-hub/kubectl@934aaa4354bbbc3d2176ae8d7cae92d515032dff # v1.35.3
env: env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }} KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with: with:
+3 -3
View File
@@ -18,11 +18,11 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with: with:
images: ghcr.io/techarohq/anubis/docs images: ghcr.io/techarohq/anubis/docs
tags: | tags: |
@@ -31,7 +31,7 @@ jobs:
- name: Build and push - name: Build and push
id: build id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with: with:
context: ./docs context: ./docs
cache-to: type=gha cache-to: type=gha
+2 -2
View File
@@ -17,9 +17,9 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- name: Check go.mod and go.sum in main directory - name: Check go.mod and go.sum in main directory
run: | run: |
+6 -6
View File
@@ -24,15 +24,15 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: "24.11.0" node-version: "24.11.0"
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- name: Cache playwright binaries - name: Cache playwright binaries
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
id: playwright-cache id: playwright-cache
with: with:
path: | path: |
@@ -55,10 +55,10 @@ jobs:
run: npm run test run: npm run test
- name: Lint with staticcheck - name: Lint with staticcheck
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0 uses: dominikh/staticcheck-action@9716614d4101e79b4340dd97b10e54d68234e431 # v1.4.1
with: with:
version: "latest" version: "latest"
- name: Govulncheck - name: Govulncheck
run: | run: |
go tool govulncheck ./... go tool govulncheck ./... ||:
+1 -1
View File
@@ -14,6 +14,6 @@ jobs:
permissions: permissions:
pull-requests: read pull-requests: read
steps: steps:
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -25,12 +25,12 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: "24.11.0" node-version: "24.11.0"
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- name: install node deps - name: install node deps
run: | run: |
@@ -26,12 +26,12 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: "24.11.0" node-version: "24.11.0"
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- name: install node deps - name: install node deps
run: | run: |
@@ -41,7 +41,7 @@ jobs:
run: | run: |
go tool yeet go tool yeet
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: packages name: packages
path: var/* path: var/*
+4 -4
View File
@@ -34,12 +34,12 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: "24.11.0" node-version: "24.11.0"
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9 - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
@@ -57,7 +57,7 @@ jobs:
run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
if: always() if: always()
with: with:
name: ${{ env.ARTIFACT_NAME }} name: ${{ env.ARTIFACT_NAME }}
+1 -1
View File
@@ -89,7 +89,7 @@ jobs:
steps: steps:
- name: check-spelling - name: check-spelling
id: spelling id: spelling
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25 uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
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
+2 -2
View File
@@ -24,13 +24,13 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Log into registry - name: Log into registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build and push - name: Build and push
run: | run: |
cd ./test/ssh-ci cd ./test/ssh-ci
+7 -6
View File
@@ -12,14 +12,15 @@ permissions:
jobs: jobs:
ssh: ssh:
if: github.repository == 'TecharoHQ/anubis' if: github.repository == 'TecharoHQ/anubis'
runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
host: host:
- riscv64 - riscv64
- ppc64le - ppc64le
- aarch64-4k #- aarch64-4k
- aarch64-16k #- aarch64-16k
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -29,15 +30,15 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install CI target SSH key - name: Install CI target SSH key
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0 uses: shimataro/ssh-key-action@87a8f067114a8ce263df83e9ed5c849953548bc3 # v2.8.1
with: with:
key: ${{ secrets.CI_SSH_KEY }} key: ${{ secrets.CI_SSH_KEY }}
name: id_rsa name: id_rsa
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }} known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: "1.25.4" go-version: "stable"
- name: Run CI - name: Run CI
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }} run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
+2 -2
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@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor
-1
View File
@@ -24,7 +24,6 @@ build: assets
lint: assets lint: assets
$(GO) vet ./... $(GO) vet ./...
$(GO) tool staticcheck ./... $(GO) tool staticcheck ./...
$(GO) tool govulncheck ./...
prebaked-build: prebaked-build:
$(GO) build -o ./var/anubis -ldflags "-X 'github.com/TecharoHQ/anubis.Version=$(VERSION)'" ./cmd/anubis $(GO) build -o ./var/anubis -ldflags "-X 'github.com/TecharoHQ/anubis.Version=$(VERSION)'" ./cmd/anubis
+24
View File
@@ -26,9 +26,21 @@ Anubis is brought to you by sponsors and donors like:
### Gold Tier ### Gold Tier
<a href="https://www.unipromos.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/unipromos.webp" alt="Unipromos" height="64" />
</a>
<a href="https://uvensys.de/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/uvensys.webp" alt="Uvensys" height="64">
</a>
<a href="https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis"> <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"> <img src="./docs/static/img/sponsors/distrust-logo.webp" alt="Distrust" height="64">
</a> </a>
<a href="https://about.gitea.com?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/gitea-logo.webp" alt="Gitea" height="64">
</a>
<a href="https://prolocation.net?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/prolocation-logo.svg" alt="Prolocation" height="64">
</a>
<a href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh"> <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"> <img src="./docs/static/img/sponsors/terminal-trove.webp" alt="Terminal Trove" height="64">
</a> </a>
@@ -58,6 +70,18 @@ Anubis is brought to you by sponsors and donors like:
height="64" height="64"
/> />
</a> </a>
<a href="https://www.anexia.com/">
<img src="./docs/static/img/sponsors/anexia-cloudsolutions-logo.webp" alt="ANEXIA Cloud Solutions" height="64">
</a>
<a href="https://dd-wrt.com/">
<img src="./docs/static/img/sponsors/ddwrt-logo.webp" alt="embeDD GmbH" height="64">
</a>
<a href="https://www.qwertiko.de?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/qwertiko-logo.webp" alt="Qwertiko" height="64">
</a>
<a href="https://wenet.pl/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="./docs/static/img/sponsors/wenet-logo.webp" alt="Wenet" height="64">
</a>
## Overview ## Overview
+1 -1
View File
@@ -1 +1 @@
1.24.0 1.25.0
+29 -127
View File
@@ -21,7 +21,6 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -32,12 +31,12 @@ import (
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
libanubis "github.com/TecharoHQ/anubis/lib" libanubis "github.com/TecharoHQ/anubis/lib"
"github.com/TecharoHQ/anubis/lib/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/metrics"
botPolicy "github.com/TecharoHQ/anubis/lib/policy" botPolicy "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/thoth" "github.com/TecharoHQ/anubis/lib/thoth"
"github.com/TecharoHQ/anubis/web" "github.com/TecharoHQ/anubis/web"
"github.com/facebookgo/flagenv" "github.com/facebookgo/flagenv"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
"github.com/prometheus/client_golang/prometheus/promhttp"
healthv1 "google.golang.org/grpc/health/grpc_health_v1" healthv1 "google.golang.org/grpc/health/grpc_health_v1"
) )
@@ -118,33 +117,6 @@ func doHealthCheck() error {
return nil return nil
} }
// parseBindNetFromAddr determine bind network and address based on the given network and address.
func parseBindNetFromAddr(address string) (string, string) {
defaultScheme := "http://"
if !strings.Contains(address, "://") {
if strings.HasPrefix(address, ":") {
address = defaultScheme + "localhost" + address
} else {
address = defaultScheme + address
}
}
bindUri, err := url.Parse(address)
if err != nil {
log.Fatal(fmt.Errorf("failed to parse bind URL: %w", err))
}
switch bindUri.Scheme {
case "unix":
return "unix", bindUri.Path
case "tcp", "http", "https":
return "tcp", bindUri.Host
default:
log.Fatal(fmt.Errorf("unsupported network scheme %s in address %s", bindUri.Scheme, address))
}
return "", address
}
func parseSameSite(s string) http.SameSite { func parseSameSite(s string) http.SameSite {
switch strings.ToLower(s) { switch strings.ToLower(s) {
case "none": case "none":
@@ -161,53 +133,6 @@ func parseSameSite(s string) http.SameSite {
return http.SameSiteDefaultMode return http.SameSiteDefaultMode
} }
func setupListener(network string, address string) (net.Listener, string) {
formattedAddress := ""
if network == "" {
// keep compatibility
network, address = parseBindNetFromAddr(address)
}
switch network {
case "unix":
formattedAddress = "unix:" + address
case "tcp":
if strings.HasPrefix(address, ":") { // assume it's just a port e.g. :4259
formattedAddress = "http://localhost" + address
} else {
formattedAddress = "http://" + address
}
default:
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
}
listener, err := net.Listen(network, address)
if err != nil {
log.Fatal(fmt.Errorf("failed to bind to %s: %w", formattedAddress, err))
}
// additional permission handling for unix sockets
if network == "unix" {
mode, err := strconv.ParseUint(*socketMode, 8, 0)
if err != nil {
listener.Close()
log.Fatal(fmt.Errorf("could not parse socket mode %s: %w", *socketMode, err))
}
err = os.Chmod(address, os.FileMode(mode))
if err != nil {
err := listener.Close()
if err != nil {
log.Printf("failed to close listener: %v", err)
}
log.Fatal(fmt.Errorf("could not change socket mode: %w", err))
}
}
return listener, formattedAddress
}
func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool) (http.Handler, error) { func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool) (http.Handler, error) {
targetUri, err := url.Parse(target) targetUri, err := url.Parse(target)
if err != nil { if err != nil {
@@ -303,11 +228,6 @@ func main() {
wg := new(sync.WaitGroup) wg := new(sync.WaitGroup)
if *metricsBind != "" {
wg.Add(1)
go metricsServer(ctx, *lg.With("subsystem", "metrics"), wg.Done)
}
var rp http.Handler var rp http.Handler
// 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) != "" {
@@ -347,6 +267,26 @@ func main() {
lg.Debug("swapped to new logger") lg.Debug("swapped to new logger")
slog.SetDefault(lg) slog.SetDefault(lg)
if *metricsBind != "" || policy.Metrics != nil {
wg.Add(1)
ms := &metrics.Server{
Config: policy.Metrics,
Log: lg,
}
if policy.Metrics == nil {
lg.Debug("migrating flags to metrics config", "bind", *metricsBind, "network", *metricsBindNetwork, "socket-mode", *socketMode)
ms.Config = &config.Metrics{
Bind: *metricsBind,
Network: *metricsBindNetwork,
SocketMode: *socketMode,
}
}
go ms.Run(ctx, wg.Done)
}
// Warn if persistent storage is used without a configured signing key // Warn if persistent storage is used without a configured signing key
if policy.Store.IsPersistent() { if policy.Store.IsPersistent() {
if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" { if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
@@ -418,8 +358,8 @@ func main() {
var redirectDomainsList []string var redirectDomainsList []string
if *redirectDomains != "" { if *redirectDomains != "" {
domains := strings.Split(*redirectDomains, ",") domains := strings.SplitSeq(*redirectDomains, ",")
for _, domain := range domains { for domain := range domains {
_, err = url.Parse(domain) _, err = url.Parse(domain)
if err != nil { if err != nil {
log.Fatalf("cannot parse redirect-domain %q: %s", domain, err.Error()) log.Fatalf("cannot parse redirect-domain %q: %s", domain, err.Error())
@@ -427,7 +367,7 @@ func main() {
redirectDomainsList = append(redirectDomainsList, strings.TrimSpace(domain)) redirectDomainsList = append(redirectDomainsList, strings.TrimSpace(domain))
} }
} else { } else {
lg.Warn("REDIRECT_DOMAINS is not set, Anubis will only redirect to the same domain a request is coming from, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains") lg.Warn("REDIRECT_DOMAINS is not set, Anubis will redirect to any domain, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains")
} }
anubis.CookieName = *cookiePrefix + "-auth" anubis.CookieName = *cookiePrefix + "-auth"
@@ -483,7 +423,11 @@ func main() {
h = internal.JA4H(h) h = internal.JA4H(h)
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()} srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
listener, listenerUrl := setupListener(*bindNetwork, *bind) listener, listenerUrl, err := internal.SetupListener(*bindNetwork, *bind, *socketMode)
if err != nil {
log.Fatalf("SetupListener(%q, %q, %q): %v", *bindNetwork, *bind, *socketMode, err)
}
lg.Info( lg.Info(
"listening", "listening",
"url", listenerUrl, "url", listenerUrl,
@@ -518,48 +462,6 @@ func main() {
wg.Wait() wg.Wait()
} }
func metricsServer(ctx context.Context, lg slog.Logger, done func()) {
defer done()
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
st, ok := internal.GetHealth("anubis")
if !ok {
slog.Error("health service anubis does not exist, file a bug")
}
switch st {
case healthv1.HealthCheckResponse_NOT_SERVING:
http.Error(w, "NOT OK", http.StatusInternalServerError)
return
case healthv1.HealthCheckResponse_SERVING:
fmt.Fprintln(w, "OK")
return
default:
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
return
}
})
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
lg.Debug("listening for metrics", "url", metricsUrl)
go func() {
<-ctx.Done()
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(c); err != nil {
log.Printf("cannot shut down: %v", err)
}
}()
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
func extractEmbedFS(fsys embed.FS, root string, destDir string) error { func extractEmbedFS(fsys embed.FS, root string, destDir string) error {
return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error { return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
+2 -4
View File
@@ -10,6 +10,7 @@ import (
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
"slices"
"strings" "strings"
"github.com/TecharoHQ/anubis/lib/config" "github.com/TecharoHQ/anubis/lib/config"
@@ -210,11 +211,8 @@ func parseRobotsTxt(input io.Reader) ([]RobotsRule, error) {
// Mark blacklisted user agents (those with "Disallow: /") // Mark blacklisted user agents (those with "Disallow: /")
for i := range rules { for i := range rules {
for _, disallow := range rules[i].Disallows { if slices.Contains(rules[i].Disallows, "/") {
if disallow == "/" {
rules[i].IsBlacklist = true rules[i].IsBlacklist = true
break
}
} }
} }
+5 -5
View File
@@ -158,8 +158,8 @@ func TestDataFileConversion(t *testing.T) {
} }
if strings.ToLower(*outputFormat) == "yaml" { if strings.ToLower(*outputFormat) == "yaml" {
var actualData []interface{} var actualData []any
var expectedData []interface{} var expectedData []any
err = yaml.Unmarshal(actualOutput, &actualData) err = yaml.Unmarshal(actualOutput, &actualData)
if err != nil { if err != nil {
@@ -178,8 +178,8 @@ func TestDataFileConversion(t *testing.T) {
t.Errorf("Output mismatch for %s\nExpected:\n%s\n\nActual:\n%s", tc.name, expectedStr, actualStr) t.Errorf("Output mismatch for %s\nExpected:\n%s\n\nActual:\n%s", tc.name, expectedStr, actualStr)
} }
} else { } else {
var actualData []interface{} var actualData []any
var expectedData []interface{} var expectedData []any
err = json.Unmarshal(actualOutput, &actualData) err = json.Unmarshal(actualOutput, &actualData)
if err != nil { if err != nil {
@@ -419,6 +419,6 @@ Disallow: /`
// compareData performs a deep comparison of two data structures, // compareData performs a deep comparison of two data structures,
// ignoring differences that are semantically equivalent in YAML/JSON // ignoring differences that are semantically equivalent in YAML/JSON
func compareData(actual, expected interface{}) bool { func compareData(actual, expected any) bool {
return reflect.DeepEqual(actual, expected) return reflect.DeepEqual(actual, expected)
} }
+9
View File
@@ -166,6 +166,15 @@ status_codes:
CHALLENGE: 200 CHALLENGE: 200
DENY: 200 DENY: 200
# # Configuration for the metrics server. See the docs for more information:
# #
# # https://anubis.techaro.lol/docs/admin/policies#metrics-server
# #
# # This is commented out by default so that command line flags take precedence.
# metrics:
# bind: ":9090"
# network: "tcp"
# Anubis can store temporary data in one of a few backends. See the storage # Anubis can store temporary data in one of a few backends. See the storage
# backends section of the docs for more information: # backends section of the docs for more information:
# #
+1
View File
@@ -8,4 +8,5 @@
- import: (data)/crawlers/marginalia.yaml - import: (data)/crawlers/marginalia.yaml
- import: (data)/crawlers/mojeekbot.yaml - import: (data)/crawlers/mojeekbot.yaml
- import: (data)/crawlers/commoncrawl.yaml - import: (data)/crawlers/commoncrawl.yaml
- import: (data)/crawlers/wikimedia-citoid.yaml
- import: (data)/crawlers/yandexbot.yaml - import: (data)/crawlers/yandexbot.yaml
+1 -1
View File
@@ -4,5 +4,5 @@
# - Claude-SearchBot: No published IP allowlist # - Claude-SearchBot: No published IP allowlist
- name: "ai-crawlers-search" - name: "ai-crawlers-search"
user_agent_regex: >- user_agent_regex: >-
OAI-SearchBot|Claude-SearchBot|PerplexityBot OAI-SearchBot|Claude-SearchBot|PerplexityBot|meta-webindexer
action: DENY action: DENY
+18
View File
@@ -0,0 +1,18 @@
# Wikimedia Foundation citation services
# https://www.mediawiki.org/wiki/Citoid
- name: wikimedia-citoid
user_agent_regex: "Citoid/WMF"
action: ALLOW
remote_addresses: [
"208.80.152.0/22",
"2620:0:860::/46",
]
- name: wikimedia-zotero-translation-server
user_agent_regex: "ZoteroTranslationServer/WMF"
action: ALLOW
remote_addresses: [
"208.80.152.0/22",
"2620:0:860::/46",
]
+37 -2
View File
@@ -11,12 +11,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
<!-- This changes the project to: -->
- Move metrics server configuration to [the policy file](./admin/policies.mdx#metrics-server).
- Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
- Instruct reverse proxies to not cache error pages.
- Fixed mixed tab/space indentation in Caddy documentation code block
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
## v1.25.0: Necron
Hey all,
I'm sure you've all been aware that things have been slowing down a little with Anubis development, and I want to apologize for that. A lot has been going on in my life lately (my blog will have a post out on Friday with more information), and as a result I haven't really had the energy to work on Anubis in publicly visible ways. There are things going on behind the scenes, but nothing is really shippable yet, sorry!
I've also been feeling some burnout in the wake of perennial waves of anger directed towards me. I'm handling it, I'll be fine, I've just had a lot going on in my life and it's been rough.
I've been missing the sense of wanderlust and discovery that comes with the artistic way I playfully develop software. I suspect that some of the stresses I've been through (setting up a complicated surgery in a country whose language you aren't fluent in is kind of an experience) have been sapping my energy. I'd gonna try to mess with things on my break, but realistically I'm probably just gonna be either watching Stargate SG-1 or doing unreasonable amounts of ocean fishing in Final Fantasy 14. Normally I'd love to keep the details about my medical state fairly private, but I'm more of a public figure now than I was this time last year so I don't really get the invisibility I'm used to for this.
I've also had a fair amount of negativity directed at me for simply being much more visible than the anonymous threat actors running the scrapers that are ruining everything, which though understandable has not helped.
Anyways, it all worked out and I'm about to be in the hospital for a week, so if things go really badly with this release please downgrade to the last version and/or upgrade to the main branch when the fix PR is inevitably merged. I hoped to have time to tame GPG and set up full release automation in the Anubis repo, but that didn't work out this time and that's okay.
If I can challenge you all to do something, go out there and try to actually create something new somehow. Combine ideas you've never mixed before. Be creative, be human, make something purely for yourself to scratch an itch that you've always had yet never gotten around to actually mending.
At the very least, try to be an example of how you want other people to act, even when you're in a situation where software written by someone else is configured to require a user agent to execute javascript to access a webpage.
Be well,
Xe
PS: if you're well-versed in FFXIV lore, the release title should give you an idea of the kind of stuff I've been going through mentally.
- Add iplist2rule tool that lets admins turn an IP address blocklist into an Anubis ruleset. - Add iplist2rule tool that lets admins turn an IP address blocklist into an Anubis ruleset.
- Add Polish locale ([#1292](https://github.com/TecharoHQ/anubis/pull/1309)) - Add Polish locale ([#1292](https://github.com/TecharoHQ/anubis/pull/1309))
- Fix honeypot and imprint links missing `BASE_PREFIX` when deployed behind a path prefix ([#1402](https://github.com/TecharoHQ/anubis/issues/1402)) - Fix honeypot and imprint links missing `BASE_PREFIX` when deployed behind a path prefix ([#1402](https://github.com/TecharoHQ/anubis/issues/1402))
- Add ANEXIA Sponsor logo to docs ([#1409](https://github.com/TecharoHQ/anubis/pull/1409))
- Improve idle performance in memory storage - Improve idle performance in memory storage
- Add HAProxy Configurations to Docs ([#1424](https://github.com/TecharoHQ/anubis/pull/1424))
<!-- This changes the project to: -->
## v1.24.0: Y'shtola Rhul ## v1.24.0: Y'shtola Rhul
+101
View File
@@ -0,0 +1,101 @@
# HAProxy
import CodeBlock from "@theme/CodeBlock";
To use Anubis with HAProxy, you have two variants:
- simple - stick Anubis between HAProxy and your application backend (simple)
- perfect if you only have a single application in general
- advanced - force Anubis challenge by default and route to the application backend by HAProxy if the challenge is correct
- useful for complex setups
- routing can be done in HAProxy
- define ACLs in HAProxy for domains, paths etc which are required/excluded regarding Anubis
- HAProxy 3.0 recommended
## Simple Variant
```mermaid
---
title: HAProxy with simple config
---
flowchart LR
T(User Traffic)
HAProxy(HAProxy Port 80/443)
Anubis
Application
T --> HAProxy
HAProxy --> Anubis
Anubis --> |Happy Traffic| Application
```
Your Anubis env file configuration may look like this:
import simpleAnubis from "!!raw-loader!./haproxy/simple-config.env";
<CodeBlock language="bash">{simpleAnubis}</CodeBlock>
The important part is that `TARGET` points to your actual application and if Anubis and HAProxy are on the same machine, a UNIX socket can be used.
Your frontend and backend configuration of HAProxy may look like the following:
import simpleHAProxy from "!!raw-loader!./haproxy/simple-haproxy.cfg";
<CodeBlock language="bash">{simpleHAProxy}</CodeBlock>
This simply enables SSL offloading, sets some useful and required headers and routes to Anubis directly.
## Advanced Variant
Due to the fact that HAProxy can decode JWT, we are able to verify the Anubis token directly in HAProxy and route the traffic to the specific backends ourselves.
Mind that rule logic to allow Git HTTP and other legit bot traffic to bypass is delegated from Anubis to HAProxy then. If required, you should implement any whitelisting in HAProxy using `acl_anubis_ignore` yourself.
In this example are three applications behind one HAProxy frontend. Only App1 and App2 are secured via Anubis; App3 is open for everyone. The path `/excluded/path` can also be accessed by anyone.
```mermaid
---
title: HAProxy with advanced config
---
flowchart LR
T(User Traffic)
HAProxy(HAProxy Port 80/443)
B1(App1)
B2(App2)
B3(App3)
Anubis
T --> HAProxy
HAProxy --> |Traffic for App1 and App2 without valid challenge| Anubis
HAProxy --> |app1.example.com | B1
HAProxy --> |app2.example.com| B2
HAProxy --> |app3.example.com| B3
```
:::note
For an improved JWT decoding performance, it's recommended to use HAProxy version 3.0 or above.
:::
Your Anubis env file configuration may look like this:
import advancedAnubis from "!!raw-loader!./haproxy/advanced-config.env";
<CodeBlock language="bash">{advancedAnubis}</CodeBlock>
It's important to use `HS512_SECRET` which HAProxy understands. Please replace `<SECRET-HERE>` with your own secret string (alphanumerical string with 128 characters recommended).
You can set Anubis to force a challenge for every request using the following policy file:
import advancedAnubisPolicy from "!!raw-loader!./haproxy/advanced-config-policy.yml";
<CodeBlock language="yaml">{advancedAnubisPolicy}</CodeBlock>
The HAProxy config file may look like this:
import advancedHAProxy from "!!raw-loader!./haproxy/advanced-haproxy.cfg";
<CodeBlock language="haproxy">{advancedHAProxy}</CodeBlock>
Please replace `<SECRET-HERE>` with the same secret from the Anubis config.
@@ -0,0 +1,15 @@
# /etc/anubis/challenge-any.yml
bots:
- name: any
action: CHALLENGE
user_agent_regex: .*
status_codes:
CHALLENGE: 403
DENY: 403
thresholds: []
dnsbl: false
@@ -0,0 +1,11 @@
# /etc/anubis/default.env
BIND=/run/anubis/default.sock
BIND_NETWORK=unix
DIFFICULTY=4
METRICS_BIND=:9090
# target is irrelevant here, backend routing happens in HAProxy
TARGET=http://0.0.0.0
HS512_SECRET=<SECRET-HERE>
COOKIE_DYNAMIC_DOMAIN=True
POLICY_FNAME=/etc/anubis/challenge-any.yml
@@ -0,0 +1,59 @@
# /etc/haproxy/haproxy.cfg
frontend FE-multiple-applications
mode http
bind :80
# ssl offloading on port 443 using a certificate from /etc/haproxy/ssl/ directory
bind :443 ssl crt /etc/haproxy/ssl/ alpn h2,http/1.1 ssl-min-ver TLSv1.2 no-tls-tickets
# set X-Real-IP header required for Anubis
http-request set-header X-Real-IP "%[src]"
# redirect HTTP to HTTPS
http-request redirect scheme https code 301 unless { ssl_fc }
# add HSTS header
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# only force Anubis challenge for app1 and app2
acl acl_anubis_required hdr(host) -i "app1.example.com"
acl acl_anubis_required hdr(host) -i "app2.example.com"
# exclude Anubis for a specific path
acl acl_anubis_ignore path /excluded/path
# use Anubis if auth cookie not found
use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ req.cook(techaro.lol-anubis-auth) -m found }
# get payload of the JWT such as algorithm, expire time, restrictions
http-request set-var(txn.anubis_jwt_alg) req.cook(techaro.lol-anubis-auth),jwt_header_query('$.alg') if acl_anubis_required !acl_anubis_ignore
http-request set-var(txn.anubis_jwt_exp) cook(techaro.lol-anubis-auth),jwt_payload_query('$.exp','int') if acl_anubis_required !acl_anubis_ignore
http-request set-var(txn.anubis_jwt_res) cook(techaro.lol-anubis-auth),jwt_payload_query('$.restriction') if acl_anubis_required !acl_anubis_ignore
http-request set-var(txn.srcip) req.fhdr(X-Real-IP) if acl_anubis_required !acl_anubis_ignore
http-request set-var(txn.now) date() if acl_anubis_required !acl_anubis_ignore
# use Anubis if JWT has wrong algorithm, is expired, restrictions don't match or isn't signed with the correct key
use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ var(txn.anubis_jwt_alg) -m str HS512 }
use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore { var(txn.anubis_jwt_exp),sub(txn.now) -m int lt 0 }
use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ var(txn.srcip),digest(sha256),hex,lower,strcmp(txn.anubis_jwt_res) eq 0 }
use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ cook(techaro.lol-anubis-auth),jwt_verify(txn.anubis_jwt_alg,"<SECRET-HERE>") -m int 1 }
# custom routing in HAProxy
use_backend BE-app1 if { hdr(host) -i "app1.example.com" }
use_backend BE-app2 if { hdr(host) -i "app2.example.com" }
use_backend BE-app3 if { hdr(host) -i "app3.example.com" }
backend BE-app1
mode http
server app1-server 127.0.0.1:3000
backend BE-app2
mode http
server app2-server 127.0.0.1:4000
backend BE-app3
mode http
server app3-server 127.0.0.1:5000
BE-anubis
mode http
server anubis /run/anubis/default.sock
@@ -0,0 +1,10 @@
# /etc/anubis/default.env
BIND=/run/anubis/default.sock
BIND_NETWORK=unix
SOCKET_MODE=0666
DIFFICULTY=4
METRICS_BIND=:9090
COOKIE_DYNAMIC_DOMAIN=true
# address and port of the actual application
TARGET=http://localhost:3000
@@ -0,0 +1,22 @@
# /etc/haproxy/haproxy.cfg
frontend FE-application
mode http
bind :80
# ssl offloading on port 443 using a certificate from /etc/haproxy/ssl/ directory
bind :443 ssl crt /etc/haproxy/ssl/ alpn h2,http/1.1 ssl-min-ver TLSv1.2 no-tls-tickets
# set X-Real-IP header required for Anubis
http-request set-header X-Real-IP "%[src]"
# redirect HTTP to HTTPS
http-request redirect scheme https code 301 unless { ssl_fc }
# add HSTS header
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# route to Anubis backend by default
default_backend BE-anubis-application
BE-anubis-application
mode http
server anubis /run/anubis/default.sock
@@ -130,3 +130,52 @@ Then point your Ingress to the Anubis port:
# diff-add # diff-add
name: anubis name: anubis
``` ```
## Envoy Gateway
If you are using envoy-gateway, the `X-Real-Ip` header is not set by default, but Anubis does require it. You can resolve this by adding the header, either on the specific `HTTPRoute` where Anubis is listening, or on the `ClientTrafficPolicy` to apply it to any number of Gateways:
HTTPRoute:
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-route
spec:
hostnames: ["app.domain.tld"]
parentRefs:
- name: envoy-external
namespace: network
sectionName: https
rules:
- backendRefs:
- identifier: *app
port: anubis
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: X-Real-Ip
value: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"
```
Applying to any number of Gateways:
```yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: envoy
spec:
headers:
earlyRequestHeaders:
set:
- name: X-Real-Ip
value: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"
clientIPDetection:
xForwardedFor:
trustedCIDRs:
- 10.96.0.0/16 # Cluster pod CIDR
targetSelectors: # These will apply to all Gateways
- group: gateway.networking.k8s.io
kind: Gateway
```
+4 -3
View File
@@ -87,15 +87,15 @@ Anubis uses these environment variables for configuration:
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. | | `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
| `ERROR_TITLE` | unset | <EO /> If set, override the translation stack to show a custom title for error pages such as "Something went wrong!". See [Customizing messages](./botstopper.mdx#customizing-messages) for more details. | | `ERROR_TITLE` | unset | <EO /> If set, override the translation stack to show a custom title for error pages such as "Something went wrong!". See [Customizing messages](./botstopper.mdx#customizing-messages) for more details. |
| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. | | `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. | | `METRICS_BIND` | `:9090` | The legacy configuration value for the network address that Anubis serves Prometheus metrics on. Please migrate this to [the policy file](./policies.mdx#metrics-server) as soon as possible. |
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. | | `METRICS_BIND_NETWORK` | `tcp` | The legacy configuration value for the address family that Anubis serves Prometheus metrics on. Please migrate this to [the policy file](./policies.mdx#metrics-server) as soon as possible. |
| `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. | | `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. | | `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. | | `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
| `OVERLAY_FOLDER` | unset | <EO /> If set, treat the given path as an [overlay folder](./botstopper.mdx#custom-images-and-css), allowing you to customize CSS, fonts, images, and add other assets to BotStopper deployments. | | `OVERLAY_FOLDER` | unset | <EO /> If set, treat the given path as an [overlay folder](./botstopper.mdx#custom-images-and-css), allowing you to customize CSS, fonts, images, and add other assets to BotStopper deployments. |
| `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. |
| `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). Leave it unset when Anubis terminates traffic directly (sidecar/standalone deployments) or redirect building will fail with `redir=null`. | | `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). Leave it unset when Anubis terminates traffic directly (sidecar/standalone deployments) or redirect building will fail with `redir=null`. |
| `REDIRECT_DOMAINS` | unset | Comma-separated list of domain names that Anubis should allow redirects to when passing a challenge. See [Redirect Domain Configuration](./configuration/redirect-domains) for more details. | | `REDIRECT_DOMAINS` | unset | Comma-separated list of domain names that Anubis should allow redirects to when passing a challenge. See [Redirect Domain Configuration](./configuration/redirect-domains.mdx) for more details. |
| `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. |
| `SLOG_LEVEL` | `INFO` | The log level for structured logging. Valid values are `DEBUG`, `INFO`, `WARN`, and `ERROR`. Set to `DEBUG` to see all requests, evaluations, and detailed diagnostic information. | | `SLOG_LEVEL` | `INFO` | The log level for structured logging. Valid values are `DEBUG`, `INFO`, `WARN`, and `ERROR`. Set to `DEBUG` to see all requests, evaluations, and detailed diagnostic information. |
| `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. |
@@ -203,6 +203,7 @@ To get Anubis filtering your traffic, you need to make sure it's added to your H
- [Kubernetes](./environments/kubernetes.mdx) - [Kubernetes](./environments/kubernetes.mdx)
- [Nginx](./environments/nginx.mdx) - [Nginx](./environments/nginx.mdx)
- [Traefik](./environments/traefik.mdx) - [Traefik](./environments/traefik.mdx)
- [HAProxy](./environments/haproxy.mdx)
:::note :::note
+1
View File
@@ -143,3 +143,4 @@ For more details on particular reverse proxies, see here:
- [Apache](./environments/apache.mdx) - [Apache](./environments/apache.mdx)
- [Nginx](./environments/nginx.mdx) - [Nginx](./environments/nginx.mdx)
- [HAProxy](./environments/haproxy.mdx)
+47
View File
@@ -117,6 +117,27 @@ remote_addresses:
- 100.64.0.0/10 - 100.64.0.0/10
``` ```
## Metrics server
Anubis includes support for [Prometheus-style metrics](https://prometheus.io/docs/introduction/overview/), allowing systems administrators to monitor Anubis' performance and effectiveness. This is a separate HTTP server with metrics, health checking, and debug routes.
Anubis' metrics server is configured with the `metrics` block in the configuration file:
```yaml
metrics:
bind: ":9090"
network: "tcp"
```
If you want to bind metrics to a Unix socket, make sure to set the network to `unix` and add a socket mode:
```yaml
metrics:
bind: "/tmp/anubis_metrics.sock"
network: unix
socketMode: "0700" # must be a string
```
## Imprint / Impressum support ## Imprint / Impressum support
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information. Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
@@ -393,6 +414,32 @@ logging:
When files are rotated out, the old files will be named after the rotation timestamp in [RFC 3339 format](https://www.rfc-editor.org/rfc/rfc3339). When files are rotated out, the old files will be named after the rotation timestamp in [RFC 3339 format](https://www.rfc-editor.org/rfc/rfc3339).
:::note
If you are running Anubis in systemd via a native package, the default systemd unit settings are very restrictive and will forbid writing to folders in `/var/log`. In order to fix this, please make a [drop-in unit](https://www.flatcar.org/docs/latest/setup/systemd/drop-in-units/) like the following:
```text
# /etc/systemd/anubis@instance-name.service.d/50-var-log-readwrite.conf
[Service]
ReadWritePaths=/run /var/log/anubis
```
Once you write this to the correct place, reload the systemd configuration:
```text
sudo systemctl daemon-reload
```
And then restart Anubis:
```text
sudo systemctl restart anubis@instance-name
```
You may be required to make drop-ins for each Anubis instance depending on the facts and circumstances of your deployment.
:::
### `stdio` sink ### `stdio` sink
By default, Anubis logs everything to the standard error stream of its process. This requires no configuration: By default, Anubis logs everything to the standard error stream of its process. This requires no configuration:
-31
View File
@@ -1,31 +0,0 @@
---
title: Code quality guidelines
---
When submitting code to Anubis, please take the time to consider the fact that this project is security software. If things go bad, bots can pummel sites into oblivion. This is not ideal for uptime.
As such, code reviews will be a bit more strict than you have seen in other projects. This is not people trying to be mean, this is a side effect of taking the problem seriously.
When making code changes, try to do the following:
- If you're submitting a bugfix, add a test case for it
- If you're changing the JavaScript, make sure the integration tests pass (`npm run test:integration`)
## Commit messages
Anubis follows the Go project's conventions for commit messages. In general, an ideal commit message should read like this:
```text
path/to/folder: brief description of the change
If the change is subtle, has implementation consequences, or is otherwise
not entirely self-describing: take the time to spell out why. If things
are very subtle, please also amend the documentation accordingly
```
The subject of a commit message should be the second half of the sentence "This commit changes the Anubis project to:". Here's a few examples:
- `disable DroneBL by default`
- `port the challenge to WebAssembly`
The extended commit message is also your place to give rationale for a new feature. When maintainers are reviewing your code, they will use this to figure out if the burden from feature maintainership is worth the merge.
+21
View File
@@ -35,9 +35,21 @@ Anubis is brought to you by sponsors and donors like:
### Gold Tier ### Gold Tier
<a href="https://www.unipromos.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="/img/sponsors/unipromos.webp" alt="Uvensys" height="64" />
</a>
<a href="https://uvensys.de/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="/img/sponsors/uvensys.webp" alt="Uvensys" height="64" />
</a>
<a href="https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis"> <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" /> <img src="/img/sponsors/distrust-logo.webp" alt="Distrust" height="64" />
</a> </a>
<a href="https://about.gitea.com?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="/img/sponsors/gitea-logo.webp" alt="Gitea" height="64" />
</a>
<a href="https://prolocation.net?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="/img/sponsors/prolocation-logo.svg" alt="Prolocation" height="64" />
</a>
<a href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh"> <a href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh">
<img <img
src="/img/sponsors/terminal-trove.webp" src="/img/sponsors/terminal-trove.webp"
@@ -75,6 +87,15 @@ Anubis is brought to you by sponsors and donors like:
height="64" height="64"
/> />
</a> </a>
<a href="https://dd-wrt.com/">
<img src="/img/sponsors/ddwrt-logo.webp" alt="embeDD GmbH" height="64" />
</a>
<a href="https://www.qwertiko.de?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="/img/sponsors/qwertiko-logo.webp" alt="Qwertiko" height="64" />
</a>
<a href="https://wenet.pl/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
<img src="/img/sponsors/wenet-logo.webp" alt="Wenet" height="64" />
</a>
## Overview ## Overview
@@ -22,3 +22,24 @@ If you use a browser extension such as [JShelter](https://jshelter.org/), you wi
## Does Anubis mine Bitcoin? ## Does Anubis mine Bitcoin?
No. Anubis does not mine Bitcoin or any other cryptocurrency. No. Anubis does not mine Bitcoin or any other cryptocurrency.
## I disabled Just-in-time compilation in my browser. Why is Anubis slow?
Anubis proof-of-work checks run an open source JavaScript program in your browser. These checks do a lot of complicated math and aim to be done quickly, so the execution speed depends on [Just-in-time (JIT) compilation](https://en.wikipedia.org/wiki/Just-in-time_compilation). JIT compiles JavaScript from the Internet into native machine code at runtime. The code produced by the JIT engine is almost as good as if it was written in a native programming language and compiled for your computer in particular. Without JIT, all JavaScript programs on every website you visit run through a slow interpreter.
This interpreter is much slower than native code because it has to translate each low level JavaScript operation into many dozens of calls to execute. This means that using the interpreter incurs a massive performance hit by its very nature; it takes longer to add numbers than if the CPU just added the numbers directly.
Some users choose to disable JIT as a hardening measure against theoretical browser exploits. This is a reasonable choice if you face targeted attacks from well-resourced adversaries (such as nation-state actors), but it comes with real performance costs.
If you've disabled JIT and find Anubis checks slow, re-enabling JIT is the fix. There is no way for Anubis to work around this on our end.
## What versions of browsers does Anubis support?
Anubis is written mainly by a single person in a basement in Canada. As such it is impossible for Anubis to support every version of every browser on the planet. As such, here's a few rules of thumb for the browsers that Anubis focuses on supporting:
- At least the two (2) most recent LTS releases of Firefox and Chrome.
- At least the version of Chromium as used by the Samsung Browser on Android.
- At least the last version of Chromium and Firefox that are known to run on Windows 7.
- At least the version of Safari that runs on the second-to-oldest iPhone model currently on the market.
We cannot give more cohesive version bounds than this. If you run into problems, please file an issue. Sometimes you may just need to upgrade hardware though.
+11 -6
View File
@@ -38,10 +38,8 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://squirreljme.cc/ - https://squirreljme.cc/
- https://superlove.sayitditto.net/ - https://superlove.sayitditto.net/
- https://svnweb.freebsd.org/ - https://svnweb.freebsd.org/
- https://trac.ffmpeg.org/
- https://tumfatig.net/ - https://tumfatig.net/
- https://wiki.archlinux.org/ - https://wiki.archlinux.org/
- https://wiki.dolphin-emu.org/
- https://wiki.freepascal.org/ - https://wiki.freepascal.org/
- https://wiki.koha-community.org/ - https://wiki.koha-community.org/
- https://www.cfaarchive.org/ - https://www.cfaarchive.org/
@@ -53,6 +51,11 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://bbs.archlinux32.org/ - https://bbs.archlinux32.org/
- https://bugs.archlinux32.org/ - https://bugs.archlinux32.org/
</details> </details>
- <details>
<summary>Dolphin Emulator</summary>
- https://forums.dolphin-emu.org/
- https://wiki.dolphin-emu.org/
</details>
- <details> - <details>
<summary>Duke University</summary> <summary>Duke University</summary>
- https://repository.duke.edu/ - https://repository.duke.edu/
@@ -60,6 +63,11 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://find.library.duke.edu/ - https://find.library.duke.edu/
- https://nicholas.duke.edu/ - https://nicholas.duke.edu/
</details> </details>
- <details>
<summary>FFmpeg</summary>
- https://git.ffmpeg.org/
- https://trac.ffmpeg.org/
</details>
- <details> - <details>
<summary>Forschungszentrum Jülich</summary> <summary>Forschungszentrum Jülich</summary>
- https://juser.fz-juelich.de/ - https://juser.fz-juelich.de/
@@ -112,11 +120,8 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://git.kernel.org/ - https://git.kernel.org/
- https://lore.kernel.org/ - https://lore.kernel.org/
</details> </details>
- <details>
<summary>The United Nations</summary>
- https://policytoolbox.iiep.unesco.org/
</details>
- <details> - <details>
<summary>Valve Corporation</summary> <summary>Valve Corporation</summary>
- https://developer.valvesoftware.com/wiki/Main_Page - https://developer.valvesoftware.com/wiki/Main_Page
- https://wiki.teamfortress.com/wiki/Main_Page
</details> </details>
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="250px" height="42px" viewBox="0 0.05 250 42" enable-background="new 0 0.05 250 42" xml:space="preserve">
<g>
<rect y="0.15" fill="#FFFFFF" width="42" height="42"/>
<polygon fill="#FF0600" points="0,42.05 10.5,42.05 10.5,10.55 31.5,10.55 31.5,31.55 21,31.55 10.5,42.05 42,42.05 42,0.05
0,0.05 "/>
<path fill="#222222" d="M59.1,24.95h-2.2v9.6h-5.3V8.05h7.5c5.7,0,7.5,3.3,7.5,8.5C66.6,21.65,64.9,24.95,59.1,24.95z M59,12.75h-2
v7.4h2c2.2,0,2.3-2,2.3-3.7C61.3,14.85,61.2,12.75,59,12.75z"/>
<path fill="#222222" d="M80.1,34.55l-3.2-10.3h-1.8v10.3h-5.3V8.05h7.6c5.8,0,7.4,3,7.4,8.1c0,2.8-0.4,5.4-3,7l3.9,11.5h-5.6V34.55
z M77.3,12.75h-2.2v6.7h2.2c2,0,2.1-1.8,2.1-3.4C79.4,14.55,79.3,12.75,77.3,12.75z"/>
<path fill="#222222" d="M101.2,32.149c-1.2,1.5-2.9,2.7-5.8,2.7s-4.6-1.2-5.8-2.7c-1.9-2.3-2-5.8-2-10.899c0-5.1,0.1-8.6,2-10.9
c1.2-1.5,2.9-2.7,5.8-2.7s4.6,1.2,5.8,2.7c1.9,2.3,2,5.8,2,10.9C103.2,26.35,103.1,29.85,101.2,32.149z M97.2,13.55
c-0.3-0.6-0.8-1-1.8-1s-1.5,0.4-1.8,1c-0.6,1.2-0.7,4.5-0.7,7.7c0,3.2,0.1,6.5,0.7,7.8c0.3,0.6,0.8,1,1.8,1s1.5-0.4,1.8-1
c0.6-1.2,0.7-4.5,0.7-7.8C97.9,18.05,97.8,14.75,97.2,13.55z"/>
<path fill="#222222" d="M106.5,34.55V8.05h5.3v21.8h6.9v4.8h-12.2V34.55z"/>
<path fill="#222222" d="M133.7,32.149c-1.2,1.5-2.9,2.7-5.8,2.7c-2.9,0-4.601-1.2-5.8-2.7c-1.9-2.3-2-5.8-2-10.899
c0-5.1,0.1-8.6,2-10.9c1.2-1.5,2.9-2.7,5.8-2.7c2.899,0,4.6,1.2,5.8,2.7c1.899,2.3,2,5.8,2,10.9
C135.7,26.35,135.6,29.85,133.7,32.149z M129.7,13.55c-0.3-0.6-0.8-1-1.8-1s-1.5,0.4-1.801,1c-0.6,1.2-0.7,4.5-0.7,7.7
c0,3.2,0.1,6.5,0.7,7.8c0.301,0.6,0.801,1,1.801,1s1.5-0.4,1.8-1c0.6-1.2,0.7-4.5,0.7-7.8C130.4,18.05,130.3,14.75,129.7,13.55z"/>
<path fill="#222222" d="M151.3,32.95c-1.3,1.3-2.899,1.899-5.1,1.899c-2.9,0-4.601-1.2-5.8-2.7c-1.9-2.3-2-5.8-2-10.899
c0-5.1,0.1-8.6,2-10.9c1.199-1.5,2.899-2.7,5.8-2.7c2.2,0,3.8,0.6,5.1,1.9c1.4,1.3,2.3,3.4,2.4,5.9h-5.3c0-0.7-0.101-1.5-0.4-2
c-0.3-0.6-0.8-1-1.8-1s-1.5,0.4-1.8,1c-0.601,1.2-0.7,4.5-0.7,7.7c0,3.2,0.1,6.5,0.7,7.8c0.3,0.6,0.8,1,1.8,1s1.5-0.4,1.8-1
c0.3-0.601,0.4-1.301,0.4-2.101h5.3C153.6,29.55,152.7,31.649,151.3,32.95z"/>
<path fill="#222222" d="M167.8,34.55l-0.899-4.1H160.8l-0.899,4.1h-5.5l6.899-26.5h5.2l6.8,26.5H167.8z M163.9,15.85l-2,9.8h4.1
L163.9,15.85z"/>
<path fill="#222222" d="M182.2,12.75v21.8h-5.3v-21.8h-4.5v-4.7H186.6v4.8H182.2V12.75z"/>
<path fill="#222222" d="M189.5,34.55V8.05h5.3v26.5H189.5z"/>
<path fill="#222222" d="M211.7,32.149c-1.2,1.5-2.9,2.7-5.8,2.7c-2.9,0-4.601-1.2-5.801-2.7c-1.899-2.3-2-5.8-2-10.899
c0-5.1,0.101-8.6,2-10.9c1.2-1.5,2.9-2.7,5.801-2.7c2.899,0,4.6,1.2,5.8,2.7c1.899,2.3,2,5.8,2,10.9
C213.7,26.35,213.5,29.85,211.7,32.149z M207.6,13.55c-0.3-0.6-0.8-1-1.8-1s-1.5,0.4-1.8,1c-0.6,1.2-0.7,4.5-0.7,7.7
c0,3.2,0.101,6.5,0.7,7.8c0.3,0.6,0.8,1,1.8,1s1.5-0.4,1.8-1c0.601-1.2,0.7-4.5,0.7-7.8C208.3,18.05,208.3,14.75,207.6,13.55z"/>
<path fill="#222222" d="M227.9,34.55l-5.601-13v13H217V8.05h4.6l5.5,13v-13h5.301v26.5H227.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+1 -1
View File
@@ -36,7 +36,7 @@ func Glob(pattern, subj string) bool {
end := len(parts) - 1 end := len(parts) - 1
// Go over the leading parts and ensure they match. // Go over the leading parts and ensure they match.
for i := 0; i < end; i++ { for i := range end {
idx := strings.Index(subj, parts[i]) idx := strings.Index(subj, parts[i])
switch i { switch i {
+2 -2
View File
@@ -184,7 +184,7 @@ func TestHashCollisions(t *testing.T) {
for _, prefix := range prefixes { for _, prefix := range prefixes {
for _, suffix := range suffixes { for _, suffix := range suffixes {
for _, variation := range variations { for _, variation := range variations {
for i := 0; i < 100; i++ { for i := range 100 {
input := fmt.Sprintf("%s%s%s-%d", prefix, suffix, variation, i) input := fmt.Sprintf("%s%s%s-%d", prefix, suffix, variation, i)
hash := XXHash64sum(input) hash := XXHash64sum(input)
if existing, exists := xxhashHashes[hash]; exists { if existing, exists := xxhashHashes[hash]; exists {
@@ -211,7 +211,7 @@ func TestHashCollisions(t *testing.T) {
seqCount := 0 seqCount := 0
for _, pattern := range patterns { for _, pattern := range patterns {
for i := 0; i < 10000; i++ { for i := range 10000 {
input := fmt.Sprintf(pattern, i) input := fmt.Sprintf(pattern, i)
hash := XXHash64sum(input) hash := XXHash64sum(input)
if existing, exists := xxhashHashes[hash]; exists { if existing, exists := xxhashHashes[hash]; exists {
+2 -2
View File
@@ -120,7 +120,7 @@ func (i *Impl) makeAffirmations() []string {
count := rand.IntN(5) + 1 count := rand.IntN(5) + 1
var result []string var result []string
for j := 0; j < count; j++ { for range count {
result = append(result, i.affirmation.Spin()) result = append(result, i.affirmation.Spin())
} }
@@ -131,7 +131,7 @@ func (i *Impl) makeSpins() []string {
count := rand.IntN(5) + 1 count := rand.IntN(5) + 1
var result []string var result []string
for j := 0; j < count; j++ { for range count {
result = append(result, i.body.Spin()) result = append(result, i.body.Spin())
} }
+1 -1
View File
@@ -16,7 +16,7 @@ func (lo *ListOr[T]) UnmarshalJSON(data []byte) error {
// Check if first non-whitespace character is '[' // Check if first non-whitespace character is '['
firstChar := data[0] firstChar := data[0]
for i := 0; i < len(data); i++ { for i := range data {
if data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { if data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
firstChar = data[i] firstChar = data[i]
break break
+2 -2
View File
@@ -95,7 +95,7 @@ func TestMemoryUsage(t *testing.T) {
// Run getTarget many times // Run getTarget many times
u, _ := url.Parse("/path/to/resource?query=1&foo=bar&baz=qux") u, _ := url.Parse("/path/to/resource?query=1&foo=bar&baz=qux")
for i := 0; i < 10000; i++ { for range 10000 {
_ = cache.getTarget(u) _ = cache.getTarget(u)
} }
@@ -129,7 +129,7 @@ func TestMemoryUsage(t *testing.T) {
runtime.GC() runtime.GC()
runtime.ReadMemStats(&m1) runtime.ReadMemStats(&m1)
for i := 0; i < 1000; i++ { for range 1000 {
_ = cache.extractOGTags(doc) _ = cache.extractOGTags(doc)
} }
+4 -9
View File
@@ -3,6 +3,7 @@ package ogtags
import ( import (
"context" "context"
"net/url" "net/url"
"slices"
"strings" "strings"
"testing" "testing"
"unicode/utf8" "unicode/utf8"
@@ -78,7 +79,7 @@ func FuzzGetTarget(f *testing.F) {
} }
// Ensure no memory corruption by calling multiple times // Ensure no memory corruption by calling multiple times
for i := 0; i < 3; i++ { for range 3 {
result2 := cache.getTarget(u) result2 := cache.getTarget(u)
if result != result2 { if result != result2 {
t.Errorf("getTarget not deterministic: %q != %q", result, result2) t.Errorf("getTarget not deterministic: %q != %q", result, result2)
@@ -148,11 +149,8 @@ func FuzzExtractOGTags(f *testing.F) {
} }
} }
if !approved { if !approved {
for _, tag := range cache.approvedTags { if slices.Contains(cache.approvedTags, property) {
if property == tag {
approved = true approved = true
break
}
} }
} }
if !approved { if !approved {
@@ -260,11 +258,8 @@ func FuzzExtractMetaTagInfo(f *testing.F) {
} }
} }
if !approved { if !approved {
for _, tag := range cache.approvedTags { if slices.Contains(cache.approvedTags, property) {
if property == tag {
approved = true approved = true
break
}
} }
} }
if !approved { if !approved {
+2 -3
View File
@@ -1,6 +1,7 @@
package ogtags package ogtags
import ( import (
"slices"
"strings" "strings"
"golang.org/x/net/html" "golang.org/x/net/html"
@@ -65,11 +66,9 @@ func (c *OGTagCache) extractMetaTagInfo(n *html.Node) (property, content string)
} }
// Check exact matches // Check exact matches
for _, tag := range c.approvedTags { if slices.Contains(c.approvedTags, propertyKey) {
if propertyKey == tag {
return propertyKey, content return propertyKey, content
} }
}
return "", content return "", content
} }
+92
View File
@@ -0,0 +1,92 @@
package internal
import (
"errors"
"fmt"
"net"
"net/url"
"os"
"strconv"
"strings"
)
// parseBindNetFromAddr determine bind network and address based on the given network and address.
func parseBindNetFromAddr(address string) (string, string, error) {
defaultScheme := "http://"
if !strings.Contains(address, "://") {
if strings.HasPrefix(address, ":") {
address = defaultScheme + "localhost" + address
} else {
address = defaultScheme + address
}
}
bindUri, err := url.Parse(address)
if err != nil {
return "", "", fmt.Errorf("failed to parse bind URL: %w", err)
}
switch bindUri.Scheme {
case "unix":
return "unix", bindUri.Path, nil
case "tcp", "http", "https":
return "tcp", bindUri.Host, nil
default:
return "", "", fmt.Errorf("unsupported network scheme %s in address %s", bindUri.Scheme, address)
}
}
// SetupListener sets up a network listener based on the input from configuration
// envvars. It returns a network listener and the URL to that listener or an error.
func SetupListener(network, address, socketMode string) (net.Listener, string, error) {
formattedAddress := ""
var err error
if network == "" {
// keep compatibility
network, address, err = parseBindNetFromAddr(address)
}
if err != nil {
return nil, "", fmt.Errorf("can't parse bind and network: %w", err)
}
switch network {
case "unix":
formattedAddress = "unix:" + address
case "tcp":
if strings.HasPrefix(address, ":") { // assume it's just a port e.g. :4259
formattedAddress = "http://localhost" + address
} else {
formattedAddress = "http://" + address
}
default:
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
}
ln, err := net.Listen(network, address)
if err != nil {
return nil, "", fmt.Errorf("failed to bind to %s: %w", formattedAddress, err)
}
// additional permission handling for unix sockets
if network == "unix" {
mode, err := strconv.ParseUint(socketMode, 8, 0)
if err != nil {
ln.Close()
return nil, "", fmt.Errorf("could not parse socket mode %s: %w", socketMode, err)
}
err = os.Chmod(address, os.FileMode(mode))
if err != nil {
err := fmt.Errorf("could not change socket mode: %w", err)
clErr := ln.Close()
if clErr != nil {
return nil, "", errors.Join(err, clErr)
}
return nil, "", err
}
}
return ln, formattedAddress, nil
}
+180
View File
@@ -0,0 +1,180 @@
package internal
import (
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)
func TestParseBindNetFromAddr(t *testing.T) {
for _, tt := range []struct {
name string
address string
wantErr bool
network string
bind string
}{
{
name: "simple tcp",
address: "localhost:9090",
wantErr: false,
network: "tcp",
bind: "localhost:9090",
},
{
name: "simple unix",
address: "unix:///tmp/foo.sock",
wantErr: false,
network: "unix",
bind: "/tmp/foo.sock",
},
{
name: "invalid network",
address: "foo:///tmp/bar.sock",
wantErr: true,
},
{
name: "tcp uri",
address: "tcp://[::]:9090",
wantErr: false,
network: "tcp",
bind: "[::]:9090",
},
{
name: "http uri",
address: "http://[::]:9090",
wantErr: false,
network: "tcp",
bind: "[::]:9090",
},
{
name: "https uri",
address: "https://[::]:9090",
wantErr: false,
network: "tcp",
bind: "[::]:9090",
},
} {
t.Run(tt.name, func(t *testing.T) {
network, bind, err := parseBindNetFromAddr(tt.address)
switch {
case tt.wantErr && err == nil:
t.Errorf("parseBindNetFromAddr(%q) should have errored but did not", tt.address)
case !tt.wantErr && err != nil:
t.Errorf("parseBindNetFromAddr(%q) threw an error: %v", tt.address, err)
}
if network != tt.network {
t.Errorf("parseBindNetFromAddr(%q) wanted network: %q, got: %q", tt.address, tt.network, network)
}
if bind != tt.bind {
t.Errorf("parseBindNetFromAddr(%q) wanted bind: %q, got: %q", tt.address, tt.bind, bind)
}
})
}
}
func TestSetupListener(t *testing.T) {
td := t.TempDir()
for _, tt := range []struct {
name string
network, address, socketMode string
wantErr bool
socketURLPrefix string
}{
{
name: "simple tcp",
network: "",
address: ":0",
wantErr: false,
socketURLPrefix: "http://localhost:",
},
{
name: "simple unix",
network: "",
address: "unix://" + filepath.Join(td, "a"),
socketMode: "0770",
wantErr: false,
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
},
{
name: "tcp",
network: "tcp",
address: ":0",
wantErr: false,
socketURLPrefix: "http://localhost:",
},
{
name: "udp",
network: "udp",
address: ":0",
wantErr: true,
socketURLPrefix: "http://localhost:",
},
{
name: "unix socket",
network: "unix",
socketMode: "0770",
address: filepath.Join(td, "a"),
wantErr: false,
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
},
{
name: "invalid socket mode",
network: "unix",
socketMode: "taco bell",
address: filepath.Join(td, "a"),
wantErr: true,
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
},
{
name: "empty socket mode",
network: "unix",
socketMode: "",
address: filepath.Join(td, "a"),
wantErr: true,
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
},
} {
t.Run(tt.name, func(t *testing.T) {
ln, socketURL, err := SetupListener(tt.network, tt.address, tt.socketMode)
switch {
case tt.wantErr && err == nil:
t.Errorf("SetupListener(%q, %q, %q) should have errored but did not", tt.network, tt.address, tt.socketMode)
case !tt.wantErr && err != nil:
t.Fatalf("SetupListener(%q, %q, %q) threw an error: %v", tt.network, tt.address, tt.socketMode, err)
}
if ln != nil {
defer ln.Close()
}
if !tt.wantErr && !strings.HasPrefix(socketURL, tt.socketURLPrefix) {
t.Errorf("SetupListener(%q, %q, %q) should have returned a URL with prefix %q but got: %q", tt.network, tt.address, tt.socketMode, tt.socketURLPrefix, socketURL)
}
if tt.socketMode != "" {
mode, err := strconv.ParseUint(tt.socketMode, 8, 0)
if err != nil {
return
}
sockPath := strings.TrimPrefix(socketURL, "unix:")
st, err := os.Stat(sockPath)
if err != nil {
t.Fatalf("can't os.Stat(%q): %v", sockPath, err)
}
if st.Mode().Perm() != fs.FileMode(mode) {
t.Errorf("file mode of %q should be %s but is actually %s", sockPath, strconv.FormatUint(mode, 8), strconv.FormatUint(uint64(st.Mode()), 8))
}
}
})
}
}
+1 -1
View File
@@ -270,7 +270,7 @@ func TestPlaywrightBrowser(t *testing.T) {
var performedAction action var performedAction action
var err error var err error
for i := 0; i < 5; i++ { for i := range 5 {
performedAction, err = executeTestCase(t, tc, typ, anubisURL) performedAction, err = executeTestCase(t, tc, typ, anubisURL)
if performedAction == tc.action { if performedAction == tc.action {
break break
+23 -4
View File
@@ -81,11 +81,11 @@ type Server struct {
func (s *Server) getTokenKeyfunc() jwt.Keyfunc { func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
// return ED25519 key if HS512 is not set // return ED25519 key if HS512 is not set
if len(s.hs512Secret) == 0 { if len(s.hs512Secret) == 0 {
return func(token *jwt.Token) (interface{}, error) { return func(token *jwt.Token) (any, error) {
return s.ed25519Priv.Public().(ed25519.PublicKey), nil return s.ed25519Priv.Public().(ed25519.PublicKey), nil
} }
} else { } else {
return func(token *jwt.Token) (interface{}, error) { return func(token *jwt.Token) (any, error) {
return s.hs512Secret, nil return s.hs512Secret, nil
} }
} }
@@ -106,6 +106,13 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
//return nil, errors.New("[unexpected] this codepath should be impossible, asked to issue a challenge for a non-challenge rule") //return nil, errors.New("[unexpected] this codepath should be impossible, asked to issue a challenge for a non-challenge rule")
} }
if rule.Challenge == nil {
rule.Challenge = &config.ChallengeRules{
Difficulty: s.policy.DefaultDifficulty,
Algorithm: config.DefaultAlgorithm,
}
}
id, err := uuid.NewV7() id, err := uuid.NewV7()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -491,7 +498,11 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
chall, err := s.getChallenge(r) chall, err := s.getChallenge(r)
if err != nil { if err != nil {
lg.Error("getChallenge failed", "err", err) lg.Error("getChallenge failed", "err", err)
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm), makeCode(err)) algorithm := "unknown"
if rule.Challenge != nil {
algorithm = rule.Challenge.Algorithm
}
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), algorithm), makeCode(err))
return return
} }
@@ -638,8 +649,16 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
} }
if matches { if matches {
challRules := t.Challenge
if challRules == nil {
// Non-CHALLENGE thresholds (ALLOW/DENY) don't have challenge config.
// Use an empty struct so hydrateChallengeRule can fill from stored
// challenge data during validation, rather than baking in defaults
// that could mismatch the difficulty the client actually solved for.
challRules = &config.ChallengeRules{}
}
return cr("threshold/"+t.Name, t.Action, weight), &policy.Bot{ return cr("threshold/"+t.Name, t.Action, weight), &policy.Bot{
Challenge: t.Challenge, Challenge: challRules,
Rules: &checker.List{}, Rules: &checker.List{},
}, nil }, nil
} }
+2 -2
View File
@@ -38,8 +38,8 @@ func NewTLogWriter(t *testing.T) io.Writer {
// Write splits input on newlines and logs each line separately. // Write splits input on newlines and logs each line separately.
func (w *TLogWriter) Write(p []byte) (n int, err error) { func (w *TLogWriter) Write(p []byte) (n int, err error) {
lines := strings.Split(string(p), "\n") lines := strings.SplitSeq(string(p), "\n")
for _, line := range lines { for line := range lines {
if line != "" { if line != "" {
w.t.Log(line) w.t.Log(line)
} }
+1
View File
@@ -10,6 +10,7 @@ var (
ErrFailed = errors.New("challenge: user failed challenge") ErrFailed = errors.New("challenge: user failed challenge")
ErrMissingField = errors.New("challenge: missing field") ErrMissingField = errors.New("challenge: missing field")
ErrInvalidFormat = errors.New("challenge: field has invalid format") ErrInvalidFormat = errors.New("challenge: field has invalid format")
ErrInvalidInput = errors.New("challenge: input is nil or missing required fields")
) )
func NewError(verb, publicReason string, privateReason error) *Error { func NewError(verb, publicReason string, privateReason error) *Error {
+33
View File
@@ -1,6 +1,7 @@
package challenge package challenge
import ( import (
"fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"sort" "sort"
@@ -50,12 +51,44 @@ type IssueInput struct {
Store store.Interface Store store.Interface
} }
func (in *IssueInput) Valid() error {
if in == nil {
return fmt.Errorf("%w: IssueInput is nil", ErrInvalidInput)
}
if in.Rule == nil {
return fmt.Errorf("%w: Rule is nil", ErrInvalidInput)
}
if in.Rule.Challenge == nil {
return fmt.Errorf("%w: Rule.Challenge is nil", ErrInvalidInput)
}
if in.Challenge == nil {
return fmt.Errorf("%w: Challenge is nil", ErrInvalidInput)
}
return nil
}
type ValidateInput struct { type ValidateInput struct {
Rule *policy.Bot Rule *policy.Bot
Challenge *Challenge Challenge *Challenge
Store store.Interface Store store.Interface
} }
func (in *ValidateInput) Valid() error {
if in == nil {
return fmt.Errorf("%w: ValidateInput is nil", ErrInvalidInput)
}
if in.Rule == nil {
return fmt.Errorf("%w: Rule is nil", ErrInvalidInput)
}
if in.Rule.Challenge == nil {
return fmt.Errorf("%w: Rule.Challenge is nil", ErrInvalidInput)
}
if in.Challenge == nil {
return fmt.Errorf("%w: Challenge is nil", ErrInvalidInput)
}
return nil
}
type Impl interface { type Impl interface {
// Setup registers any additional routes with the Impl for assets or API routes. // Setup registers any additional routes with the Impl for assets or API routes.
Setup(mux *http.ServeMux) Setup(mux *http.ServeMux)
+8
View File
@@ -24,6 +24,10 @@ type Impl struct{}
func (i *Impl) Setup(mux *http.ServeMux) {} func (i *Impl) Setup(mux *http.ServeMux) {}
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) { func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) {
if err := in.Valid(); err != nil {
return nil, err
}
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
if err != nil { if err != nil {
return nil, fmt.Errorf("can't render page: %w", err) return nil, fmt.Errorf("can't render page: %w", err)
@@ -49,6 +53,10 @@ func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
} }
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error { func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
if err := in.Valid(); err != nil {
return challenge.NewError("validate", "invalid input", err)
}
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 800 * time.Millisecond) wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 800 * time.Millisecond)
if time.Now().Before(wantTime) { if time.Now().Before(wantTime) {
+8
View File
@@ -39,6 +39,10 @@ type impl struct{}
func (i *impl) Setup(mux *http.ServeMux) {} func (i *impl) Setup(mux *http.ServeMux) {}
func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) { func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) {
if err := in.Valid(); err != nil {
return nil, err
}
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
if err != nil { if err != nil {
return nil, fmt.Errorf("can't render page: %w", err) return nil, fmt.Errorf("can't render page: %w", err)
@@ -57,6 +61,10 @@ func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
} }
func (i *impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error { func (i *impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
if err := in.Valid(); err != nil {
return challenge.NewError("validate", "invalid input", err)
}
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 80 * time.Millisecond) wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 80 * time.Millisecond)
if time.Now().Before(wantTime) { if time.Now().Before(wantTime) {
+4
View File
@@ -33,6 +33,10 @@ func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
} }
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error { func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
if err := in.Valid(); err != nil {
return chall.NewError("validate", "invalid input", err)
}
rule := in.Rule rule := in.Rule
challenge := in.Challenge.RandomData challenge := in.Challenge.RandomData
@@ -30,6 +30,62 @@ func mkRequest(t *testing.T, values map[string]string) *http.Request {
return req return req
} }
// TestValidateNilRuleChallenge reproduces the panic from
// https://github.com/TecharoHQ/anubis/issues/1463
//
// When a threshold rule matches during PassChallenge, check() can return
// a policy.Bot with Challenge == nil. After hydrateChallengeRule fails to
// run (or the error path hits before it), Validate dereferences
// rule.Challenge.Difficulty and panics.
func TestValidateNilRuleChallenge(t *testing.T) {
i := &Impl{Algorithm: "fast"}
lg := slog.With()
// This is the exact response for SHA256("hunter" + "0") with 0 leading zeros required.
const challengeStr = "hunter"
const response = "2652bdba8fb4d2ab39ef28d8534d7694c557a4ae146c1e9237bd8d950280500e"
req := mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "69",
"response": response,
})
for _, tc := range []struct {
name string
input *challenge.ValidateInput
}{
{
name: "nil-rule-challenge",
input: &challenge.ValidateInput{
Rule: &policy.Bot{},
Challenge: &challenge.Challenge{RandomData: challengeStr},
},
},
{
name: "nil-rule",
input: &challenge.ValidateInput{
Challenge: &challenge.Challenge{RandomData: challengeStr},
},
},
{
name: "nil-challenge",
input: &challenge.ValidateInput{Rule: &policy.Bot{Challenge: &config.ChallengeRules{Algorithm: "fast"}}},
},
{
name: "nil-input",
input: nil,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := i.Validate(req, lg, tc.input)
if !errors.Is(err, challenge.ErrInvalidInput) {
t.Fatalf("expected ErrInvalidInput, got: %v", err)
}
})
}
}
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
i := &Impl{Algorithm: "fast"} i := &Impl{Algorithm: "fast"}
bot := &policy.Bot{ bot := &policy.Bot{
+13 -4
View File
@@ -19,7 +19,7 @@ import (
var ( var (
ErrNoBotRulesDefined = errors.New("config: must define at least one (1) bot rule") ErrNoBotRulesDefined = errors.New("config: must define at least one (1) bot rule")
ErrBotMustHaveName = errors.New("config.Bot: must set name") ErrBotMustHaveName = errors.New("config.Bot: must set name")
ErrBotMustHaveUserAgentOrPath = errors.New("config.Bot: must set either user_agent_regex, path_regex, headers_regex, or remote_addresses") ErrBotMustHaveUserAgentOrPath = errors.New("config.Bot: must set one of user_agent_regex, path_regex, headers_regex, remote_addresses, expression, or Thoth keyword")
ErrBotMustHaveUserAgentOrPathNotBoth = errors.New("config.Bot: must set either user_agent_regex, path_regex, and not both") ErrBotMustHaveUserAgentOrPathNotBoth = errors.New("config.Bot: must set either user_agent_regex, path_regex, and not both")
ErrUnknownAction = errors.New("config.Bot: unknown action") ErrUnknownAction = errors.New("config.Bot: unknown action")
ErrInvalidUserAgentRegex = errors.New("config.Bot: invalid user agent regex") ErrInvalidUserAgentRegex = errors.New("config.Bot: invalid user agent regex")
@@ -228,8 +228,8 @@ type ImportStatement struct {
} }
func (is *ImportStatement) open() (fs.File, error) { func (is *ImportStatement) open() (fs.File, error) {
if strings.HasPrefix(is.Import, "(data)/") { if after, ok := strings.CutPrefix(is.Import, "(data)/"); ok {
fname := strings.TrimPrefix(is.Import, "(data)/") fname := after
fin, err := data.BotPolicies.Open(fname) fin, err := data.BotPolicies.Open(fname)
return fin, err return fin, err
} }
@@ -325,7 +325,7 @@ func (sc StatusCodes) Valid() error {
} }
type fileConfig struct { type fileConfig struct {
OpenGraph openGraphFileConfig `json:"openGraph,omitempty"` OpenGraph openGraphFileConfig `json:"openGraph"`
Impressum *Impressum `json:"impressum,omitempty"` Impressum *Impressum `json:"impressum,omitempty"`
Store *Store `json:"store"` Store *Store `json:"store"`
Bots []BotOrImport `json:"bots"` Bots []BotOrImport `json:"bots"`
@@ -334,6 +334,7 @@ type fileConfig struct {
DNSBL bool `json:"dnsbl"` DNSBL bool `json:"dnsbl"`
DNSTTL DnsTTL `json:"dns_ttl"` DNSTTL DnsTTL `json:"dns_ttl"`
Logging *Logging `json:"logging"` Logging *Logging `json:"logging"`
Metrics *Metrics `json:"metrics,omitempty"`
} }
func (c *fileConfig) Valid() error { func (c *fileConfig) Valid() error {
@@ -375,6 +376,12 @@ func (c *fileConfig) Valid() error {
} }
} }
if c.Metrics != nil {
if err := c.Metrics.Valid(); err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 { if len(errs) != 0 {
return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...)) return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
} }
@@ -417,6 +424,7 @@ func Load(fin io.Reader, fname string) (*Config, error) {
StatusCodes: c.StatusCodes, StatusCodes: c.StatusCodes,
Store: c.Store, Store: c.Store,
Logging: c.Logging, Logging: c.Logging,
Metrics: c.Metrics,
} }
if c.OpenGraph.TimeToLive != "" { if c.OpenGraph.TimeToLive != "" {
@@ -508,6 +516,7 @@ type Config struct {
Logging *Logging Logging *Logging
DNSBL bool DNSBL bool
DNSTTL DnsTTL DNSTTL DnsTTL
Metrics *Metrics
} }
func (c Config) Valid() error { func (c Config) Valid() error {
-3
View File
@@ -188,7 +188,6 @@ func TestBotValid(t *testing.T) {
} }
for _, cs := range tests { for _, cs := range tests {
cs := cs
t.Run(cs.name, func(t *testing.T) { t.Run(cs.name, func(t *testing.T) {
err := cs.bot.Valid() err := cs.bot.Valid()
if err == nil && cs.err == nil { if err == nil && cs.err == nil {
@@ -216,7 +215,6 @@ func TestConfigValidKnownGood(t *testing.T) {
} }
for _, st := range finfos { for _, st := range finfos {
st := st
t.Run(st.Name(), func(t *testing.T) { t.Run(st.Name(), func(t *testing.T) {
fin, err := os.Open(filepath.Join("testdata", "good", st.Name())) fin, err := os.Open(filepath.Join("testdata", "good", st.Name()))
if err != nil { if err != nil {
@@ -303,7 +301,6 @@ func TestConfigValidBad(t *testing.T) {
} }
for _, st := range finfos { for _, st := range finfos {
st := st
t.Run(st.Name(), func(t *testing.T) { t.Run(st.Name(), func(t *testing.T) {
fin, err := os.Open(filepath.Join("testdata", "bad", st.Name())) fin, err := os.Open(filepath.Join("testdata", "bad", st.Name()))
if err != nil { if err != nil {
+54
View File
@@ -0,0 +1,54 @@
package config
import (
"errors"
"fmt"
"strconv"
)
var (
ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration")
ErrNoMetricsBind = errors.New("config.Metrics: must define bind")
ErrNoMetricsNetwork = errors.New("config.Metrics: must define network")
ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets")
ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode")
ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network")
)
type Metrics struct {
Bind string `json:"bind" yaml:"bind"`
Network string `json:"network" yaml:"network"`
SocketMode string `json:"socketMode" yaml:"socketMode"`
}
func (m *Metrics) Valid() error {
var errs []error
if m.Bind == "" {
errs = append(errs, ErrNoMetricsBind)
}
if m.Network == "" {
errs = append(errs, ErrNoMetricsNetwork)
}
switch m.Network {
case "tcp", "tcp4", "tcp6": // https://pkg.go.dev/net#Listen
case "unix":
if m.SocketMode == "" {
errs = append(errs, ErrNoMetricsSocketMode)
}
if _, err := strconv.ParseUint(m.SocketMode, 8, 0); err != nil {
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidMetricsSocketMode, err))
}
default:
errs = append(errs, ErrInvalidMetricsNetwork)
}
if len(errs) != 0 {
return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...))
}
return nil
}
+87
View File
@@ -0,0 +1,87 @@
package config
import (
"errors"
"testing"
)
func TestMetricsValid(t *testing.T) {
for _, tt := range []struct {
name string
input *Metrics
err error
}{
{
name: "basic TCP",
input: &Metrics{
Bind: ":9090",
Network: "tcp",
},
},
{
name: "basic TCP4",
input: &Metrics{
Bind: ":9090",
Network: "tcp4",
},
},
{
name: "basic TCP6",
input: &Metrics{
Bind: ":9090",
Network: "tcp6",
},
},
{
name: "basic unix",
input: &Metrics{
Bind: "/tmp/anubis-metrics.sock",
Network: "unix",
SocketMode: "0770",
},
},
{
name: "no bind",
input: &Metrics{},
err: ErrNoMetricsBind,
},
{
name: "no network",
input: &Metrics{},
err: ErrNoMetricsNetwork,
},
{
name: "no unix socket mode",
input: &Metrics{
Bind: "/tmp/anubis-metrics.sock",
Network: "unix",
},
err: ErrNoMetricsSocketMode,
},
{
name: "invalid unix socket mode",
input: &Metrics{
Bind: "/tmp/anubis-metrics.sock",
Network: "unix",
SocketMode: "taco bell",
},
err: ErrInvalidMetricsSocketMode,
},
{
name: "invalid network",
input: &Metrics{
Bind: ":9090",
Network: "taco",
},
err: ErrInvalidMetricsNetwork,
},
} {
t.Run(tt.name, func(t *testing.T) {
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
t.Logf("wanted error: %v", tt.err)
t.Logf("got error: %v", err)
t.Error("validation failed")
}
})
}
}
+3
View File
@@ -0,0 +1,3 @@
metrics:
bind: ":9090"
network: taco
+5 -1
View File
@@ -5,5 +5,9 @@
"remote_addresses": ["0.0.0.0/0", "::/0"], "remote_addresses": ["0.0.0.0/0", "::/0"],
"action": "ALLOW" "action": "ALLOW"
} }
] ],
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
+4
View File
@@ -4,3 +4,7 @@ bots:
- "0.0.0.0/0" - "0.0.0.0/0"
- "::/0" - "::/0"
action: ALLOW action: ALLOW
metrics:
bind: ":9090"
network: "tcp"
+5 -1
View File
@@ -8,5 +8,9 @@
"action": "DENY" "action": "DENY"
} }
], ],
"dnsbl": false "dnsbl": false,
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
+4
View File
@@ -3,3 +3,7 @@ bots:
headers_regex: headers_regex:
CF-Worker: .* CF-Worker: .*
action: DENY action: DENY
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -4,3 +4,7 @@ bots:
asns: asns:
match: match:
- 13335 # Cloudflare - 13335 # Cloudflare
metrics:
bind: ":9090"
network: "tcp"
+5 -1
View File
@@ -5,5 +5,9 @@
"user_agent_regex": "Mozilla", "user_agent_regex": "Mozilla",
"action": "CHALLENGE" "action": "CHALLENGE"
} }
] ],
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
+4
View File
@@ -2,3 +2,7 @@ bots:
- name: generic-browser - name: generic-browser
user_agent_regex: Mozilla user_agent_regex: Mozilla
action: CHALLENGE action: CHALLENGE
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -6,3 +6,7 @@ bots:
- name: "test" - name: "test"
user_agent_regex: ".*" user_agent_regex: ".*"
action: "DENY" action: "DENY"
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -6,3 +6,7 @@ bots:
- '"Accept" in headers' - '"Accept" in headers'
- headers["Accept"].contains("text/html") - headers["Accept"].contains("text/html")
- randInt(1) == 0 - randInt(1) == 0
metrics:
bind: ":9090"
network: "tcp"
+5 -1
View File
@@ -6,5 +6,9 @@
"action": "DENY" "action": "DENY"
} }
], ],
"dnsbl": false "dnsbl": false,
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
+4
View File
@@ -2,3 +2,7 @@ bots:
- name: everything - name: everything
user_agent_regex: .* user_agent_regex: .*
action: DENY action: DENY
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -4,3 +4,7 @@ bots:
geoip: geoip:
countries: countries:
- US - US
metrics:
bind: ":9090"
network: "tcp"
+5 -1
View File
@@ -10,5 +10,9 @@
] ]
} }
} }
] ],
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
+4
View File
@@ -6,3 +6,7 @@ bots:
- userAgent.startsWith("git/") || userAgent.contains("libgit") - userAgent.startsWith("git/") || userAgent.contains("libgit")
- > - >
"Git-Protocol" in headers && headers["Git-Protocol"] == "version=2" "Git-Protocol" in headers && headers["Git-Protocol"] == "version=2"
metrics:
bind: ":9090"
network: "tcp"
+5 -1
View File
@@ -3,5 +3,9 @@
{ {
"import": "./testdata/hack-test.json" "import": "./testdata/hack-test.json"
} }
] ],
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
+4
View File
@@ -1,2 +1,6 @@
bots: bots:
- import: ./testdata/hack-test.yaml - import: ./testdata/hack-test.yaml
metrics:
bind: ":9090"
network: "tcp"
+5 -1
View File
@@ -3,5 +3,9 @@
{ {
"import": "(data)/common/keep-internet-working.yaml" "import": "(data)/common/keep-internet-working.yaml"
} }
] ],
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
@@ -1,2 +1,6 @@
bots: bots:
- import: (data)/common/keep-internet-working.yaml - import: (data)/common/keep-internet-working.yaml
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -8,3 +8,7 @@ impressum:
page: page:
title: Test title: Test
body: <p>This is a test</p> body: <p>This is a test</p>
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -13,3 +13,7 @@ logs:
oldFileTimeFormat: 2006-01-02T15-04-05 # RFC 3339-ish oldFileTimeFormat: 2006-01-02T15-04-05 # RFC 3339-ish
compress: true compress: true
useLocalTime: false # timezone for rotated files is UTC useLocalTime: false # timezone for rotated files is UTC
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -5,3 +5,7 @@ bots:
logging: logging:
sink: "stdio" sink: "stdio"
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -6,3 +6,7 @@ bots:
adjust: 5 adjust: 5
thresholds: [] thresholds: []
metrics:
bind: ":9090"
network: "tcp"
+5 -1
View File
@@ -75,5 +75,9 @@
"user_agent_regex": "Mozilla", "user_agent_regex": "Mozilla",
"action": "CHALLENGE" "action": "CHALLENGE"
} }
] ],
"metrics": {
"bind": ":9090",
"network": "tcp"
}
} }
+4
View File
@@ -10,3 +10,7 @@ openGraph:
default: default:
"og:title": "Xe's magic land of fun" "og:title": "Xe's magic land of fun"
"og:description": "We're no strangers to love, you know the rules and so do I" "og:description": "We're no strangers to love, you know the rules and so do I"
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -4,3 +4,7 @@ bots:
user_agent_regex: Mozilla user_agent_regex: Mozilla
weight: weight:
adjust: 5 adjust: 5
metrics:
bind: ":9090"
network: "tcp"
+4
View File
@@ -9,5 +9,9 @@
"status_codes": { "status_codes": {
"CHALLENGE": 200, "CHALLENGE": 200,
"DENY": 200 "DENY": 200
},
"metrics": {
"bind": ":9090",
"network": "tcp"
} }
} }

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