mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-07 17:28:17 +00:00
Compare commits
31 Commits
v1.17.0-be
...
Xe/express
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6c361c294 | ||
|
|
e9e602976f | ||
|
|
33bb5803a8 | ||
|
|
ada7b3a179 | ||
|
|
dfa7025afe | ||
|
|
884af5fd4c | ||
|
|
3fb8fa2009 | ||
|
|
b43df36f7d | ||
|
|
fd058964fa | ||
|
|
fb20b36b18 | ||
|
|
84cba05167 | ||
|
|
9f988578a4 | ||
|
|
ea4e5751ab | ||
|
|
4e1db3842e | ||
|
|
029c79ba28 | ||
|
|
9f8ede7fe3 | ||
|
|
80bd7c563b | ||
|
|
92a3e5ba81 | ||
|
|
65cbc6922c | ||
|
|
eae3a7b5e4 | ||
|
|
6daf08216e | ||
|
|
bd0e46dac3 | ||
|
|
76514f9f32 | ||
|
|
b0f0913ea2 | ||
|
|
5423ab013a | ||
|
|
301c7a42bd | ||
|
|
755c18a9a7 | ||
|
|
0fa9906e3a | ||
|
|
b08580ca33 | ||
|
|
d8f923974e | ||
|
|
ef52550e70 |
6
.github/workflows/docs-deploy.yml
vendored
6
.github/workflows/docs-deploy.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: ./docs
|
||||
cache-to: type=gha
|
||||
@@ -49,14 +49,14 @@ jobs:
|
||||
push: true
|
||||
|
||||
- name: Apply k8s manifests to aeacus
|
||||
uses: actions-hub/kubectl@9270913c29699788b51bc04becd0ebdf048ffb49 # v1.32.3
|
||||
uses: actions-hub/kubectl@e81783053d902f50d752d21a6d99cf9689a652e1 # v1.33.0
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.AEACUS_KUBECONFIG }}
|
||||
with:
|
||||
args: apply -k docs/manifest
|
||||
|
||||
- name: Apply k8s manifests to aeacus
|
||||
uses: actions-hub/kubectl@9270913c29699788b51bc04becd0ebdf048ffb49 # v1.32.3
|
||||
uses: actions-hub/kubectl@e81783053d902f50d752d21a6d99cf9689a652e1 # v1.33.0
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.AEACUS_KUBECONFIG }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/docs-test.yml
vendored
2
.github/workflows/docs-test.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: ./docs
|
||||
cache-to: type=gha
|
||||
|
||||
2
.github/workflows/package-builds-stable.yml
vendored
2
.github/workflows/package-builds-stable.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Build Packages
|
||||
run: |
|
||||
wget https://github.com/TecharoHQ/yeet/releases/download/v0.1.1/yeet_0.1.1_amd64.deb -O var/yeet.deb
|
||||
wget https://github.com/TecharoHQ/yeet/releases/download/v0.2.1/yeet_0.2.1_amd64.deb -O var/yeet.deb
|
||||
sudo apt -y install -f ./var/yeet.deb
|
||||
rm ./var/yeet.deb
|
||||
yeet
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
|
||||
- name: Build Packages
|
||||
run: |
|
||||
wget https://github.com/TecharoHQ/yeet/releases/download/v0.1.1/yeet_0.1.1_amd64.deb -O var/yeet.deb
|
||||
wget https://github.com/TecharoHQ/yeet/releases/download/v0.2.1/yeet_0.2.1_amd64.deb -O var/yeet.deb
|
||||
sudo apt -y install -f ./var/yeet.deb
|
||||
rm ./var/yeet.deb
|
||||
yeet
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
@@ -22,7 +22,7 @@ Anubis [weighs the soul of your connection](https://en.wikipedia.org/wiki/Weighi
|
||||
|
||||
This program is designed to help protect the small internet from the endless storm of requests that flood in from AI companies. Anubis is as lightweight as possible to ensure that everyone can afford to protect the communities closest to them.
|
||||
|
||||
Anubis is a bit of a nuclear response. This will result in your website being blocked from smaller scrapers and may inhibit "good bots" like the Internet Archive. You can configure [bot policy definitions](./admin/policies.mdx) to explicitly allowlist them and we are working on a curated set of "known good" bots to allow for a compromise between discoverability and uptime.
|
||||
Anubis is a bit of a nuclear response. This will result in your website being blocked from smaller scrapers and may inhibit "good bots" like the Internet Archive. You can configure [bot policy definitions](./docs/docs/admin/policies.mdx) to explicitly allowlist them and we are working on a curated set of "known good" bots to allow for a compromise between discoverability and uptime.
|
||||
|
||||
In most cases, you should not need this and can probably get by using Cloudflare to protect a given origin. However, for circumstances where you can't or won't use Cloudflare, Anubis is there for you.
|
||||
|
||||
|
||||
6
data/apps/allow-api-routes.yaml
Normal file
6
data/apps/allow-api-routes.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
- name: allow-api-routes
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- '!(method == "HEAD" || method == "GET")'
|
||||
- path.startsWith("/api/")
|
||||
@@ -12,6 +12,12 @@
|
||||
{
|
||||
"import": "(data)/bots/us-ai-scraper.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/bots/aggressive-brazilian-scrapers.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/clients/curl-impersonate.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/googlebot.yaml"
|
||||
},
|
||||
@@ -41,9 +47,9 @@
|
||||
},
|
||||
{
|
||||
"name": "generic-browser",
|
||||
"user_agent_regex": "Mozilla|Opera\n",
|
||||
"user_agent_regex": "Mozilla|Opera",
|
||||
"action": "CHALLENGE"
|
||||
}
|
||||
],
|
||||
"dnsbl": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ bots:
|
||||
- import: (data)/bots/cloudflare-workers.yaml
|
||||
- import: (data)/bots/headless-browsers.yaml
|
||||
- import: (data)/bots/us-ai-scraper.yaml
|
||||
- import: (data)/bots/aggressive-brazilian-scrapers.yaml
|
||||
- import: (data)/clients/curl-impersonate.yaml
|
||||
|
||||
# Search engines to allow
|
||||
- import: (data)/crawlers/googlebot.yaml
|
||||
@@ -41,10 +43,11 @@ bots:
|
||||
# report_as: 4 # lie to the operator
|
||||
# algorithm: slow # intentionally waste CPU cycles and time
|
||||
|
||||
# Generic catchall rule
|
||||
- name: generic-browser
|
||||
user_agent_regex: >
|
||||
Mozilla|Opera
|
||||
action: CHALLENGE
|
||||
# Challenge clients with "Mozilla" or "Opera" in their user-agent string
|
||||
#- import: (data)/common/legacy-challenge-everything.yaml
|
||||
|
||||
- name: reject-browsers
|
||||
action: DENY
|
||||
expression: userAgent.isBrowserLike()
|
||||
|
||||
dnsbl: false
|
||||
|
||||
28
data/bots/aggressive-brazilian-scrapers.yaml
Normal file
28
data/bots/aggressive-brazilian-scrapers.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
- name: deny-aggressive-brazilian-scrapers
|
||||
action: DENY
|
||||
expression:
|
||||
any:
|
||||
# Internet Explorer should be out of support
|
||||
- userAgent.contains("MSIE")
|
||||
# Trident is the Internet Explorer browser engine
|
||||
- userAgent.contains("Trident")
|
||||
# Opera is a fork of chrome now
|
||||
- userAgent.contains("Presto")
|
||||
# Windows CE is discontinued
|
||||
- userAgent.contains("Windows CE")
|
||||
# Windows 95 is discontinued
|
||||
- userAgent.contains("Windows 95")
|
||||
# Windows 98 is discontinued
|
||||
- userAgent.contains("Windows 98")
|
||||
# Windows 9.x is discontinued
|
||||
- userAgent.contains("Win 9x")
|
||||
# Amazon does not have an Alexa Toolbar.
|
||||
- userAgent.contains("Alexa Toolbar")
|
||||
- name: challenge-aggressive-brazilian-scrapers
|
||||
action: CHALLENGE
|
||||
expression:
|
||||
any:
|
||||
# This is not released, even Windows 11 calls itself Windows 10
|
||||
- userAgent.contains("Windows NT 11.0")
|
||||
# iPods are not in common use
|
||||
- userAgent.contains("iPod")
|
||||
@@ -1,4 +1,4 @@
|
||||
- name: "ai-robots-txt"
|
||||
user_agent_regex: >
|
||||
AI2Bot|Ai2Bot-Dolma|Amazonbot|anthropic-ai|Applebot|Applebot-Extended|Brightbot 1.0|Bytespider|CCBot|ChatGPT-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|FriendlyCrawler|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|ISSCyberRiskCrawler|Kangaroo Bot|Meta-ExternalAgent|Meta-ExternalFetcher|OAI-SearchBot|omgili|omgilibot|PanguBot|Perplexity-User|PerplexityBot|PetalBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|Timpibot|VelenPublicWebCrawler|Webzio-Extended|YouBot
|
||||
action: DENY
|
||||
user_agent_regex: >-
|
||||
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Applebot|Applebot-Extended|Brightbot 1.0|Bytespider|CCBot|ChatGPT-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|NovaAct|OAI-SearchBot|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|YouBot
|
||||
action: DENY
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
- name: cloudflare-workers
|
||||
headers_regex:
|
||||
CF-Worker: .*
|
||||
action: DENY
|
||||
expression: '"Cf-Worker" in headers'
|
||||
action: CHALLENGE
|
||||
9
data/bots/irc-bots/archlinux-phrik.yaml
Normal file
9
data/bots/irc-bots/archlinux-phrik.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# phrik in the Arch Linux IRC channels
|
||||
- name: archlinux-phrik
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- remoteAddress == "159.69.213.214"
|
||||
- userAgent == "Mozilla/5.0 (compatible; utils.web Limnoria module)"
|
||||
- '"X-Http-Version" in headers'
|
||||
- headers["X-Http-Version"] == "HTTP/1.1"
|
||||
9
data/bots/irc-bots/gentoo-chat.yaml
Normal file
9
data/bots/irc-bots/gentoo-chat.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# chat in the gentoo IRC channels
|
||||
- name: gentoo-chat
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- remoteAddress == "45.76.166.57"
|
||||
- userAgent == "Mozilla/5.0 (Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
|
||||
- '"X-Http-Version" in headers'
|
||||
- headers["X-Http-Version"] == "HTTP/1.1"
|
||||
32
data/clients/curl-impersonate.yaml
Normal file
32
data/clients/curl-impersonate.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
- name: curl-impersonate
|
||||
action: CHALLENGE
|
||||
expression:
|
||||
any:
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '" Not A;Brand";v="99", "Chromium";v="101", "Microsoft Edge";v="101"'
|
||||
- >
|
||||
"Sec-Ch-Ua" in headers && headers["Sec-Ch-Ua"] == '" Not A;Brand";v="99", "Chromium";v="99", "Microsoft Edge";v="99"'
|
||||
14
data/clients/git.yaml
Normal file
14
data/clients/git.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
- name: allow-git-clients
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- >
|
||||
(
|
||||
userAgent.startsWith("git/") ||
|
||||
userAgent.contains("libgit") ||
|
||||
userAgent.startsWith("go-git") ||
|
||||
userAgent.startsWith("JGit/") ||
|
||||
userAgent.startsWith("JGit-")
|
||||
)
|
||||
- '"Git-Protocol" in headers'
|
||||
- headers["Git-Protocol"] == "version=2"
|
||||
7
data/clients/go-get.yaml
Normal file
7
data/clients/go-get.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
- name: go-get
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.startsWith("Go-http-client/")
|
||||
- '"go-get" in query'
|
||||
- query["go-get"] == "1"
|
||||
6
data/common/allow-api-like.yaml
Normal file
6
data/common/allow-api-like.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
- name: allow-api-routes
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- '!(method == "HEAD" || method == "GET")'
|
||||
- path.startsWith("/api/")
|
||||
10
data/common/challenge-browser-like.yaml
Normal file
10
data/common/challenge-browser-like.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Challenge anything with HTTP/1.1 that claims to be a browser
|
||||
- name: challenge-lies-browser-but-http-1.1
|
||||
action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- '"X-Http-Version" in headers'
|
||||
- headers["X-Http-Version"] == "HTTP/1.1"
|
||||
- '"X-Forwarded-Proto" in headers'
|
||||
- headers["X-Forwarded-Proto"] == "https"
|
||||
- userAgent.isBrowserLike()
|
||||
@@ -7,4 +7,7 @@
|
||||
action: ALLOW
|
||||
- name: robots-txt
|
||||
path_regex: ^/robots.txt$
|
||||
action: ALLOW
|
||||
- name: sitemap
|
||||
path_regex: ^/sitemap.xml$
|
||||
action: ALLOW
|
||||
4
data/common/legacy-challenge-everything.yaml
Normal file
4
data/common/legacy-challenge-everything.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generic catchall rule
|
||||
- name: generic-browser
|
||||
expression: userAgent.isBrowserLike()
|
||||
action: CHALLENGE
|
||||
3
data/common/rfc-violations.yaml
Normal file
3
data/common/rfc-violations.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
- name: no-user-agent-string
|
||||
expression: userAgent == ""
|
||||
action: DENY
|
||||
@@ -3,6 +3,6 @@ package data
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed botPolicies.yaml botPolicies.json apps bots common crawlers
|
||||
//go:embed botPolicies.yaml botPolicies.json apps bots clients common crawlers
|
||||
BotPolicies embed.FS
|
||||
)
|
||||
|
||||
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## v1.17.0: Asahi sas Brutus
|
||||
|
||||
- Ensure regexes can't end in newlines ([#372](https://github.com/TecharoHQ/anubis/issues/372))
|
||||
- Add documentation for default allow behavior (implicit rule)
|
||||
- Enable [importing configuration snippets](./admin/configuration/import.mdx) ([#321](https://github.com/TecharoHQ/anubis/pull/321))
|
||||
- Refactor check logic to be more generic and work on a Checker type
|
||||
|
||||
25
docs/docs/admin/configuration/expressions.mdx
Normal file
25
docs/docs/admin/configuration/expressions.mdx
Normal file
@@ -0,0 +1,25 @@
|
||||
# Expression-based rule matching
|
||||
|
||||
- Anubis offers the ability to use [Common Expression Language (CEL)](https://cel.dev) for advanced rule matching
|
||||
- A brief summary of CEL
|
||||
- Imagine the rule as the contents of a function body in programming or the WHERE clause in SQL
|
||||
- This is an advanced feature and it is easy to get yourself into trouble with it
|
||||
- Link to the spec, mention docs are WIP
|
||||
- Variables exposed to Anubis expressions
|
||||
- `remoteAddress` -> string IP of client
|
||||
- `host` -> string HTTP/TLS hostname
|
||||
- `method` -> string HTTP method
|
||||
- `userAgent` -> string User-Agent header
|
||||
- `path` -> string HTTP request path
|
||||
- `query` -> map[string]string URL key values
|
||||
- `headers` -> map[string]string HTTP request headers
|
||||
- Load average:
|
||||
- `load_1m` -> system load in the last minute
|
||||
- `load_5m` -> system load in the last 5 minutes
|
||||
- `load_15m` -> system load in the last 15 minutes
|
||||
- Functions exposed to Anubis expressions
|
||||
- `userAgent.isBrowserLike` -> returns true if the userAgent is like a browser
|
||||
- Life advice
|
||||
- When in doubt, throw a CHALLENGE over a DENY. CHALLENGE makes it more easy to renege
|
||||
- Example usage
|
||||
- [How to make Anubis much less aggressive](../less-aggressive.mdx)
|
||||
@@ -91,7 +91,8 @@ Assuming you are protecting `anubistest.techaro.lol`, you need the following ser
|
||||
# These headers need to be set or else Anubis will
|
||||
# throw an "admin misconfiguration" error.
|
||||
RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR}
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
RequestHeader set "X-Forwarded-Proto" "https"
|
||||
RequestHeader set "X-Http-Version" "%{SERVER_PROTOCOL}s"
|
||||
|
||||
ProxyPreserveHost On
|
||||
|
||||
|
||||
@@ -59,8 +59,14 @@ server {
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
location / {
|
||||
# Anubis needs these headers to understand the connection
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Http-Version $server_protocol;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_pass http://anubis;
|
||||
}
|
||||
|
||||
|
||||
97
docs/docs/admin/less-aggressive.mdx
Normal file
97
docs/docs/admin/less-aggressive.mdx
Normal file
@@ -0,0 +1,97 @@
|
||||
# How to make Anubis much less aggressive
|
||||
|
||||
Out of the box, Anubis has fairly paranoid defaults. It's designed to stop the bleeding now, so it defaults to a global "challenge everything" rule. This does work, but comes at significant user experience cost if users disable JavaScript or run plugins that interfere with JavaScript execution.
|
||||
|
||||
Anubis ships with a rule named `challenge-lies-browser-but-http-1.1` that changes the default behavior to fire much less often. This works on top of [expression support](./configuration/expressions.mdx) to allow you to block the worst of the bad while leaving normal users able to access the website. This requires integration with your HTTP load balancer.
|
||||
|
||||
You can import this rule by replacing the `generic-browser` rule with the following:
|
||||
|
||||
```yaml
|
||||
- import: (data)/common/challenge-browser-like.yaml
|
||||
```
|
||||
|
||||
## The new rule
|
||||
|
||||
Previously Anubis aggressively challenged everything that had "Mozilla" in its User-Agent string. The rule has been amended to this set of heuristics:
|
||||
|
||||
1. If the request headers contain `X-Http-Protocol`
|
||||
1. AND if the request header `X-Http-Protocol` is `HTTP/1.1`
|
||||
1. AND if the request headers contain `X-Forwarded-Proto`
|
||||
1. AND if the request header `X-Forwarded-Proto` is `https`
|
||||
1. AND if the request's User-Agent string is similar to that of a browser
|
||||
1. THEN throw a challenge.
|
||||
|
||||
This means that users that are using up to date browsers will automatically get through without having to pass a challenge.
|
||||
|
||||
## Apache
|
||||
|
||||
Ensure [`mod_http2`](https://httpd.apache.org/docs/2.4/mod/mod_http2.html) is loaded.
|
||||
|
||||
Make sure that your HTTPS VirtualHost has the right settings for Anubis in place:
|
||||
|
||||
```python
|
||||
# Enable HTTP/2 support so Anubis can issues challenges for HTTP/1.1 clients
|
||||
Protocols h2 http/1.1
|
||||
|
||||
# These headers need to be set or else Anubis will
|
||||
# throw an "admin misconfiguration" error.
|
||||
# diff-add
|
||||
RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR}
|
||||
# diff-add
|
||||
RequestHeader set "X-Forwarded-Proto" "https"
|
||||
# diff-add
|
||||
RequestHeader set "X-Http-Version" "%{SERVER_PROTOCOL}s"
|
||||
```
|
||||
|
||||
## Caddy
|
||||
|
||||
Make sure that your [`reverse_proxy` has the right headers configured](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#headers):
|
||||
|
||||
```python
|
||||
ellenjoe.int.within.lgbt {
|
||||
# ...
|
||||
# diff-remove
|
||||
reverse_proxy http://localhost:3000
|
||||
# diff-add
|
||||
reverse_proxy http://localhost:3000 {
|
||||
# diff-add
|
||||
header_up X-Real-Ip {remote_host}
|
||||
# diff-add
|
||||
header_up X-Http-Version {http.request.proto}
|
||||
# diff-add
|
||||
}
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
## ingress-nginx
|
||||
|
||||
Edit your `ingress-nginx-controller` ConfigMap:
|
||||
|
||||
```yaml
|
||||
data:
|
||||
# ...
|
||||
# diff-add
|
||||
location-snippet: |
|
||||
# diff-add
|
||||
proxy_set_header X-Http-Version $server_protocol;
|
||||
# diff-add
|
||||
proxy_set_header X-Tls-Version $ssl_protocol;
|
||||
```
|
||||
|
||||
## Nginx
|
||||
|
||||
Edit your `server` blocks to add the following headers:
|
||||
|
||||
```python
|
||||
# diff-add
|
||||
proxy_set_header Host $host;
|
||||
# diff-add
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# diff-add
|
||||
proxy_set_header X-Http-Version $server_protocol;
|
||||
```
|
||||
|
||||
## Traefik
|
||||
|
||||
This configuration is not currently supported with Traefik. A Traefik plugin is needed to add the right header.
|
||||
@@ -29,9 +29,9 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
||||
- https://wiki.archlinux.org/
|
||||
- https://git.devuan.org/
|
||||
- https://hydra.nixos.org/
|
||||
|
||||
- https://hydra.nixos.org/
|
||||
- https://codeberg.org/
|
||||
- <details>
|
||||
<summary>The United Nations</summary>
|
||||
|
||||
- https://policytoolbox.iiep.unesco.org/
|
||||
</details>
|
||||
|
||||
23
go.mod
23
go.mod
@@ -6,52 +6,65 @@ require (
|
||||
github.com/a-h/templ v0.3.857
|
||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/cel-go v0.25.0
|
||||
github.com/playwright-community/playwright-go v0.5101.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||
github.com/shirou/gopsutil/v4 v4.25.3
|
||||
github.com/yl2chen/cidranger v1.0.2
|
||||
golang.org/x/net v0.39.0
|
||||
k8s.io/apimachinery v0.32.3
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.23.1 // indirect
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/natefinch/atomic v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
k8s.io/apimachinery v0.32.3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/a-h/templ/cmd/templ
|
||||
golang.org/x/tools/cmd/goimports
|
||||
golang.org/x/tools/cmd/stringer
|
||||
honnef.co/go/tools/cmd/staticcheck
|
||||
)
|
||||
|
||||
46
go.sum
46
go.sum
@@ -1,3 +1,5 @@
|
||||
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
|
||||
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
@@ -6,6 +8,8 @@ github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg=
|
||||
github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
@@ -14,11 +18,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 h1:CkmB2l68uhvRlwOTPrwnuitSxi/S3Cg4L5QYOcL9MBc=
|
||||
@@ -33,15 +40,23 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
|
||||
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@@ -60,6 +75,8 @@ github.com/playwright-community/playwright-go v0.5101.0/go.mod h1:kBNWs/w2aJ2ZUp
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
@@ -68,19 +85,30 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+uQBDeAhHhwdzB5fSlVdhGcM=
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
|
||||
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -97,10 +125,12 @@ golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -122,16 +152,24 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -3,6 +3,7 @@ package internal
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -22,3 +23,14 @@ func InitSlog(level string) {
|
||||
})
|
||||
slog.SetDefault(slog.New(h))
|
||||
}
|
||||
|
||||
func GetRequestLogger(r *http.Request) *slog.Logger {
|
||||
return slog.With(
|
||||
"user_agent", r.UserAgent(),
|
||||
"accept_language", r.Header.Get("Accept-Language"),
|
||||
"priority", r.Header.Get("Priority"),
|
||||
"x-forwarded-for",
|
||||
r.Header.Get("X-Forwarded-For"),
|
||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
||||
)
|
||||
}
|
||||
|
||||
341
lib/anubis.go
341
lib/anubis.go
@@ -2,38 +2,31 @@ package lib
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
"github.com/TecharoHQ/anubis/decaymap"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/TecharoHQ/anubis/xess"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -64,121 +57,6 @@ var (
|
||||
})
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Next http.Handler
|
||||
Policy *policy.ParsedConfig
|
||||
RedirectDomains []string
|
||||
ServeRobotsTXT bool
|
||||
PrivateKey ed25519.PrivateKey
|
||||
|
||||
CookieDomain string
|
||||
CookieName string
|
||||
CookiePartitioned bool
|
||||
|
||||
OGPassthrough bool
|
||||
OGTimeToLive time.Duration
|
||||
Target string
|
||||
|
||||
WebmasterEmail string
|
||||
BasePrefix string
|
||||
}
|
||||
|
||||
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||
var fin io.ReadCloser
|
||||
var err error
|
||||
|
||||
if fname != "" {
|
||||
fin, err = os.Open(fname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
||||
}
|
||||
} else {
|
||||
fname = "(data)/botPolicies.yaml"
|
||||
fin, err = data.BotPolicies.Open("botPolicies.yaml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[unexpected] can't parse builtin policy file %s: %w", fname, err)
|
||||
}
|
||||
}
|
||||
|
||||
defer func(fin io.ReadCloser) {
|
||||
err := fin.Close()
|
||||
if err != nil {
|
||||
slog.Error("failed to close policy file", "file", fname, "err", err)
|
||||
}
|
||||
}(fin)
|
||||
|
||||
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
||||
|
||||
return anubisPolicy, err
|
||||
}
|
||||
|
||||
func New(opts Options) (*Server, error) {
|
||||
if opts.PrivateKey == nil {
|
||||
slog.Debug("opts.PrivateKey not set, generating a new one")
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lib: can't generate private key: %v", err)
|
||||
}
|
||||
opts.PrivateKey = priv
|
||||
}
|
||||
|
||||
anubis.BasePrefix = opts.BasePrefix
|
||||
|
||||
result := &Server{
|
||||
next: opts.Next,
|
||||
priv: opts.PrivateKey,
|
||||
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
||||
policy: opts.Policy,
|
||||
opts: opts,
|
||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
xess.Mount(mux)
|
||||
|
||||
// Helper to add global prefix
|
||||
registerWithPrefix := func(pattern string, handler http.Handler, method string) {
|
||||
if method != "" {
|
||||
method = method + " " // methods must end with a space to register with them
|
||||
}
|
||||
|
||||
// Ensure there's no double slash when concatenating BasePrefix and pattern
|
||||
basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
|
||||
prefix := method + basePrefix
|
||||
|
||||
// If pattern doesn't start with a slash, add one
|
||||
if !strings.HasPrefix(pattern, "/") {
|
||||
pattern = "/" + pattern
|
||||
}
|
||||
|
||||
mux.Handle(prefix+pattern, handler)
|
||||
}
|
||||
|
||||
// Ensure there's no double slash when concatenating BasePrefix and StaticPath
|
||||
stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
|
||||
registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
|
||||
|
||||
if opts.ServeRobotsTXT {
|
||||
registerWithPrefix("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
||||
}), "GET")
|
||||
registerWithPrefix("/.well-known/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
||||
}), "GET")
|
||||
}
|
||||
|
||||
registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
|
||||
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
||||
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
||||
registerWithPrefix(anubis.APIPrefix+"test-error", http.HandlerFunc(result.TestError), "GET")
|
||||
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
||||
|
||||
result.mux = mux
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
next http.Handler
|
||||
@@ -190,40 +68,6 @@ type Server struct {
|
||||
OGTags *ogtags.OGTagCache
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
||||
if s.next == nil {
|
||||
redir := r.FormValue("redir")
|
||||
urlParsed, err := r.URL.Parse(redir)
|
||||
if err != nil {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect URL not parseable", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host) {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
} else if urlParsed.Host != r.URL.Host {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if redir != "" {
|
||||
http.Redirect(w, r, redir, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
templ.Handler(
|
||||
web.Base("You are not a bot!", web.StaticHappy()),
|
||||
).ServeHTTP(w, r)
|
||||
} else {
|
||||
s.next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
||||
fp := sha256.Sum256(s.priv.Seed())
|
||||
|
||||
@@ -248,19 +92,12 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
||||
lg := slog.With(
|
||||
"user_agent", r.UserAgent(),
|
||||
"accept_language", r.Header.Get("Accept-Language"),
|
||||
"priority", r.Header.Get("Priority"),
|
||||
"x-forwarded-for",
|
||||
r.Header.Get("X-Forwarded-For"),
|
||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
||||
)
|
||||
lg := internal.GetRequestLogger(r)
|
||||
|
||||
cr, rule, err := s.check(r)
|
||||
if err != nil {
|
||||
lg.Error("check failed", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -271,52 +108,11 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
||||
|
||||
ip := r.Header.Get("X-Real-Ip")
|
||||
|
||||
if s.policy.DNSBL && ip != "" {
|
||||
resp, ok := s.DNSBLCache.Get(ip)
|
||||
if !ok {
|
||||
lg.Debug("looking up ip in dnsbl")
|
||||
resp, err := dnsbl.Lookup(ip)
|
||||
if err != nil {
|
||||
lg.Error("can't look up ip in dnsbl", "err", err)
|
||||
}
|
||||
s.DNSBLCache.Set(ip, resp, 24*time.Hour)
|
||||
droneBLHits.WithLabelValues(resp.String()).Inc()
|
||||
}
|
||||
|
||||
if resp != dnsbl.AllGood {
|
||||
lg.Info("DNSBL hit", "status", resp.String())
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), s.opts.WebmasterEmail)), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if s.handleDNSBL(w, r, ip, lg) {
|
||||
return
|
||||
}
|
||||
|
||||
switch cr.Rule {
|
||||
case config.RuleAllow:
|
||||
lg.Debug("allowing traffic to origin (explicit)")
|
||||
s.ServeHTTPNext(w, r)
|
||||
return
|
||||
case config.RuleDeny:
|
||||
s.ClearCookie(w)
|
||||
lg.Info("explicit deny")
|
||||
if rule == nil {
|
||||
lg.Error("rule is nil, cannot calculate checksum")
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
hash := rule.Hash()
|
||||
|
||||
lg.Debug("rule hash", "hash", hash)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("Access Denied: error code %s", hash), s.opts.WebmasterEmail)), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
|
||||
return
|
||||
case config.RuleChallenge:
|
||||
lg.Debug("challenge requested")
|
||||
case config.RuleBenchmark:
|
||||
lg.Debug("serving benchmark page")
|
||||
s.RenderBench(w, r)
|
||||
return
|
||||
default:
|
||||
s.ClearCookie(w)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
if s.checkRules(w, r, cr, lg, rule) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -357,53 +153,64 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
||||
s.ServeHTTPNext(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
|
||||
if returnHTTPStatusOnly {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Authorization required"))
|
||||
return
|
||||
}
|
||||
|
||||
lg := slog.With(
|
||||
"user_agent", r.UserAgent(),
|
||||
"accept_language", r.Header.Get("Accept-Language"),
|
||||
"priority", r.Header.Get("Priority"),
|
||||
"x-forwarded-for",
|
||||
r.Header.Get("X-Forwarded-For"),
|
||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
||||
)
|
||||
|
||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
|
||||
var ogTags map[string]string = nil
|
||||
if s.opts.OGPassthrough {
|
||||
var err error
|
||||
ogTags, err = s.OGTags.GetOGTags(r.URL)
|
||||
if err != nil {
|
||||
lg.Error("failed to get OG tags", "err", err)
|
||||
ogTags = nil
|
||||
func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool {
|
||||
switch cr.Rule {
|
||||
case config.RuleAllow:
|
||||
lg.Debug("allowing traffic to origin (explicit)")
|
||||
s.ServeHTTPNext(w, r)
|
||||
return true
|
||||
case config.RuleDeny:
|
||||
s.ClearCookie(w)
|
||||
lg.Info("explicit deny")
|
||||
if rule == nil {
|
||||
lg.Error("rule is nil, cannot calculate checksum")
|
||||
s.respondWithError(w, r, "Internal Server Error: Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.RuleDeny\"")
|
||||
return true
|
||||
}
|
||||
}
|
||||
hash := rule.Hash()
|
||||
|
||||
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
||||
if err != nil {
|
||||
lg.Error("render failed", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
lg.Debug("rule hash", "hash", hash)
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), http.StatusOK)
|
||||
return true
|
||||
case config.RuleChallenge:
|
||||
lg.Debug("challenge requested")
|
||||
case config.RuleBenchmark:
|
||||
lg.Debug("serving benchmark page")
|
||||
s.RenderBench(w, r)
|
||||
return true
|
||||
default:
|
||||
s.ClearCookie(w)
|
||||
slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule)
|
||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"")
|
||||
return true
|
||||
}
|
||||
|
||||
handler := internal.NoStoreCache(templ.Handler(component))
|
||||
handler.ServeHTTP(w, r)
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
|
||||
templ.Handler(
|
||||
web.Base("Benchmarking Anubis!", web.Bench()),
|
||||
).ServeHTTP(w, r)
|
||||
func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, lg *slog.Logger) bool {
|
||||
if s.policy.DNSBL && ip != "" {
|
||||
resp, ok := s.DNSBLCache.Get(ip)
|
||||
if !ok {
|
||||
lg.Debug("looking up ip in dnsbl")
|
||||
resp, err := dnsbl.Lookup(ip)
|
||||
if err != nil {
|
||||
lg.Error("can't look up ip in dnsbl", "err", err)
|
||||
}
|
||||
s.DNSBLCache.Set(ip, resp, 24*time.Hour)
|
||||
droneBLHits.WithLabelValues(resp.String()).Inc()
|
||||
}
|
||||
|
||||
if resp != dnsbl.AllGood {
|
||||
lg.Info("DNSBL hit", "status", resp.String())
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), http.StatusOK)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
lg := slog.With("user_agent", r.UserAgent(), "accept_language", r.Header.Get("Accept-Language"), "priority", r.Header.Get("Priority"), "x-forwarded-for", r.Header.Get("X-Forwarded-For"), "x-real-ip", r.Header.Get("X-Real-Ip"))
|
||||
lg := internal.GetRequestLogger(r)
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
cr, rule, err := s.check(r)
|
||||
@@ -441,19 +248,13 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
lg := slog.With(
|
||||
"user_agent", r.UserAgent(),
|
||||
"accept_language", r.Header.Get("Accept-Language"),
|
||||
"priority", r.Header.Get("Priority"),
|
||||
"x-forwarded-for", r.Header.Get("X-Forwarded-For"),
|
||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
||||
)
|
||||
lg := internal.GetRequestLogger(r)
|
||||
|
||||
redir := r.FormValue("redir")
|
||||
redirURL, err := url.ParseRequestURI(redir)
|
||||
if err != nil {
|
||||
lg.Error("invalid redirect", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid redirect", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "Invalid redirect")
|
||||
return
|
||||
}
|
||||
// used by the path checker rule
|
||||
@@ -462,7 +263,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
cr, rule, err := s.check(r)
|
||||
if err != nil {
|
||||
lg.Error("check failed", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".\"")
|
||||
return
|
||||
}
|
||||
lg = lg.With("check_result", cr)
|
||||
@@ -471,7 +272,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if nonceStr == "" {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("no nonce")
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "missing nonce")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -479,7 +280,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if elapsedTimeStr == "" {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("no elapsedTime")
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "missing elapsedTime")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -487,7 +288,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("elapsedTime doesn't parse", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "invalid elapsedTime")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -497,15 +298,11 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
response := r.FormValue("response")
|
||||
urlParsed, err := r.URL.Parse(redir)
|
||||
if err != nil {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect URL not parseable", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "Redirect URL not parseable")
|
||||
return
|
||||
}
|
||||
|
||||
if len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host) {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
} else if urlParsed.Host != r.URL.Host {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host {
|
||||
s.respondWithError(w, r, "Redirect domain not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -515,7 +312,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("nonce doesn't parse", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "invalid nonce")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -525,7 +322,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("hash does not match", "got", response, "want", calculated)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
||||
failedValidations.Inc()
|
||||
return
|
||||
}
|
||||
@@ -534,7 +331,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
||||
failedValidations.Inc()
|
||||
return
|
||||
}
|
||||
@@ -557,7 +354,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
lg.Error("failed to sign JWT", "err", err)
|
||||
s.ClearCookie(w)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, "failed to sign JWT")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -578,7 +375,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) TestError(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.FormValue("err")
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(err, s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
s.respondWithError(w, r, err)
|
||||
}
|
||||
|
||||
func cr(name string, rule config.Rule) policy.CheckResult {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
)
|
||||
|
||||
func loadPolicies(t *testing.T, fname string) *policy.ParsedConfig {
|
||||
@@ -83,7 +84,7 @@ func TestCVE2025_24369(t *testing.T) {
|
||||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: "local.cetacean.club",
|
||||
CookieDomain: ".local.cetacean.club",
|
||||
CookiePartitioned: true,
|
||||
CookieName: t.Name(),
|
||||
})
|
||||
@@ -393,3 +394,42 @@ func TestBasePrefix(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudflareWorkersRule(t *testing.T) {
|
||||
for _, variant := range []string{"cel", "header"} {
|
||||
t.Run(variant, func(t *testing.T) {
|
||||
pol := loadPolicies(t, "./testdata/cloudflare-workers-"+variant+".yaml")
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "OK")
|
||||
})
|
||||
|
||||
s, err := New(Options{
|
||||
Next: h,
|
||||
Policy: pol,
|
||||
ServeRobotsTXT: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
}
|
||||
|
||||
t.Run("no-cf-worker-header", func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Header.Add("X-Real-Ip", "127.0.0.1")
|
||||
|
||||
cr, _, err := s.check(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cr.Rule != config.RuleAllow {
|
||||
t.Errorf("rule is wrong, wanted %s, got: %s", config.RuleAllow, cr.Rule)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
138
lib/config.go
Normal file
138
lib/config.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
"github.com/TecharoHQ/anubis/decaymap"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/TecharoHQ/anubis/xess"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Next http.Handler
|
||||
Policy *policy.ParsedConfig
|
||||
RedirectDomains []string
|
||||
ServeRobotsTXT bool
|
||||
PrivateKey ed25519.PrivateKey
|
||||
|
||||
CookieDomain string
|
||||
CookieName string
|
||||
CookiePartitioned bool
|
||||
|
||||
OGPassthrough bool
|
||||
OGTimeToLive time.Duration
|
||||
Target string
|
||||
|
||||
WebmasterEmail string
|
||||
BasePrefix string
|
||||
}
|
||||
|
||||
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||
var fin io.ReadCloser
|
||||
var err error
|
||||
|
||||
if fname != "" {
|
||||
fin, err = os.Open(fname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
||||
}
|
||||
} else {
|
||||
fname = "(data)/botPolicies.yaml"
|
||||
fin, err = data.BotPolicies.Open("botPolicies.yaml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[unexpected] can't parse builtin policy file %s: %w", fname, err)
|
||||
}
|
||||
}
|
||||
|
||||
defer func(fin io.ReadCloser) {
|
||||
err := fin.Close()
|
||||
if err != nil {
|
||||
slog.Error("failed to close policy file", "file", fname, "err", err)
|
||||
}
|
||||
}(fin)
|
||||
|
||||
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
||||
|
||||
return anubisPolicy, err
|
||||
}
|
||||
|
||||
func New(opts Options) (*Server, error) {
|
||||
if opts.PrivateKey == nil {
|
||||
slog.Debug("opts.PrivateKey not set, generating a new one")
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lib: can't generate private key: %v", err)
|
||||
}
|
||||
opts.PrivateKey = priv
|
||||
}
|
||||
|
||||
anubis.BasePrefix = opts.BasePrefix
|
||||
|
||||
result := &Server{
|
||||
next: opts.Next,
|
||||
priv: opts.PrivateKey,
|
||||
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
||||
policy: opts.Policy,
|
||||
opts: opts,
|
||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
xess.Mount(mux)
|
||||
|
||||
// Helper to add global prefix
|
||||
registerWithPrefix := func(pattern string, handler http.Handler, method string) {
|
||||
if method != "" {
|
||||
method = method + " " // methods must end with a space to register with them
|
||||
}
|
||||
|
||||
// Ensure there's no double slash when concatenating BasePrefix and pattern
|
||||
basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
|
||||
prefix := method + basePrefix
|
||||
|
||||
// If pattern doesn't start with a slash, add one
|
||||
if !strings.HasPrefix(pattern, "/") {
|
||||
pattern = "/" + pattern
|
||||
}
|
||||
|
||||
mux.Handle(prefix+pattern, handler)
|
||||
}
|
||||
|
||||
// Ensure there's no double slash when concatenating BasePrefix and StaticPath
|
||||
stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
|
||||
registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
|
||||
|
||||
if opts.ServeRobotsTXT {
|
||||
registerWithPrefix("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
||||
}), "GET")
|
||||
registerWithPrefix("/.well-known/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
||||
}), "GET")
|
||||
}
|
||||
|
||||
registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
|
||||
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
||||
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
||||
registerWithPrefix(anubis.APIPrefix+"test-error", http.HandlerFunc(result.TestError), "GET")
|
||||
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
||||
|
||||
result.mux = mux
|
||||
|
||||
return result, nil
|
||||
}
|
||||
82
lib/http.go
82
lib/http.go
@@ -2,8 +2,14 @@ package lib
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/a-h/templ"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
)
|
||||
|
||||
@@ -33,3 +39,79 @@ func (t UnixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.URL.Scheme = "http" // make http.Transport happy and avoid an infinite recursion
|
||||
return t.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
|
||||
if returnHTTPStatusOnly {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Authorization required"))
|
||||
return
|
||||
}
|
||||
|
||||
lg := internal.GetRequestLogger(r)
|
||||
|
||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
|
||||
var ogTags map[string]string = nil
|
||||
if s.opts.OGPassthrough {
|
||||
var err error
|
||||
ogTags, err = s.OGTags.GetOGTags(r.URL)
|
||||
if err != nil {
|
||||
lg.Error("failed to get OG tags", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
||||
if err != nil {
|
||||
lg.Error("render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
|
||||
s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"")
|
||||
return
|
||||
}
|
||||
|
||||
handler := internal.NoStoreCache(templ.Handler(component))
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
|
||||
templ.Handler(
|
||||
web.Base("Benchmarking Anubis!", web.Bench()),
|
||||
).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, message string) {
|
||||
s.respondWithStatus(w, r, message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(msg, s.opts.WebmasterEmail)), templ.WithStatus(status)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
||||
if s.next == nil {
|
||||
redir := r.FormValue("redir")
|
||||
urlParsed, err := r.URL.Parse(redir)
|
||||
if err != nil {
|
||||
s.respondWithStatus(w, r, "Redirect URL not parseable", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host {
|
||||
s.respondWithStatus(w, r, "Redirect domain not allowed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if redir != "" {
|
||||
http.Redirect(w, r, redir, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
templ.Handler(
|
||||
web.Base("You are not a bot!", web.StaticHappy()),
|
||||
).ServeHTTP(w, r)
|
||||
} else {
|
||||
s.next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
114
lib/policy/celchecker.go
Normal file
114
lib/policy/celchecker.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/expressions"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
type CELChecker struct {
|
||||
src string
|
||||
program cel.Program
|
||||
}
|
||||
|
||||
func NewCELChecker(cfg *config.ExpressionOrList) (*CELChecker, error) {
|
||||
env, err := expressions.NewEnvironment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var src string
|
||||
var ast *cel.Ast
|
||||
|
||||
if cfg.Expression != "" {
|
||||
src = cfg.Expression
|
||||
var iss *cel.Issues
|
||||
interm, iss := env.Compile(src)
|
||||
if iss != nil {
|
||||
return nil, iss.Err()
|
||||
}
|
||||
|
||||
ast, iss = env.Check(interm)
|
||||
if iss != nil {
|
||||
return nil, iss.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.All) != 0 {
|
||||
ast, err = expressions.Join(env, expressions.JoinAnd, cfg.All...)
|
||||
}
|
||||
|
||||
if len(cfg.Any) != 0 {
|
||||
ast, err = expressions.Join(env, expressions.JoinOr, cfg.Any...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
program, err := expressions.Compile(env, ast)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't compile CEL program: %w", err)
|
||||
}
|
||||
|
||||
return &CELChecker{
|
||||
src: src,
|
||||
program: program,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cc *CELChecker) Hash() string {
|
||||
return internal.SHA256sum(cc.src)
|
||||
}
|
||||
|
||||
func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
||||
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if val, ok := result.(types.Bool); ok {
|
||||
return bool(val), nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type CELRequest struct {
|
||||
*http.Request
|
||||
}
|
||||
|
||||
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
||||
|
||||
func (cr *CELRequest) ResolveName(name string) (any, bool) {
|
||||
switch name {
|
||||
case "remoteAddress":
|
||||
return cr.Header.Get("X-Real-Ip"), true
|
||||
case "host":
|
||||
return cr.Host, true
|
||||
case "method":
|
||||
return cr.Method, true
|
||||
case "userAgent":
|
||||
return cr.UserAgent(), true
|
||||
case "path":
|
||||
return cr.URL.Path, true
|
||||
case "query":
|
||||
return expressions.URLValues{Values: cr.URL.Query()}, true
|
||||
case "headers":
|
||||
return expressions.HTTPHeaders{Header: cr.Header}, true
|
||||
case "load_1m":
|
||||
return expressions.Load1(), true
|
||||
case "load_5m":
|
||||
return expressions.Load5(), true
|
||||
case "load_15m":
|
||||
return expressions.Load15(), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
@@ -110,11 +110,11 @@ func NewUserAgentChecker(rexStr string) (Checker, error) {
|
||||
}
|
||||
|
||||
func NewHeaderMatchesChecker(header, rexStr string) (Checker, error) {
|
||||
rex, err := regexp.Compile(rexStr)
|
||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
||||
}
|
||||
return &HeaderMatchesChecker{header, rex, internal.SHA256sum(header + ": " + rexStr)}, nil
|
||||
return &HeaderMatchesChecker{strings.TrimSpace(header), rex, internal.SHA256sum(header + ": " + rexStr)}, nil
|
||||
}
|
||||
|
||||
func (hmc *HeaderMatchesChecker) Check(r *http.Request) (bool, error) {
|
||||
@@ -135,7 +135,7 @@ type PathChecker struct {
|
||||
}
|
||||
|
||||
func NewPathChecker(rexStr string) (Checker, error) {
|
||||
rex, err := regexp.Compile(rexStr)
|
||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func (pc *PathChecker) Hash() string {
|
||||
}
|
||||
|
||||
func NewHeaderExistsChecker(key string) Checker {
|
||||
return headerExistsChecker{key}
|
||||
return headerExistsChecker{strings.TrimSpace(key)}
|
||||
}
|
||||
|
||||
type headerExistsChecker struct {
|
||||
@@ -180,11 +180,11 @@ func NewHeadersChecker(headermap map[string]string) (Checker, error) {
|
||||
|
||||
for key, rexStr := range headermap {
|
||||
if rexStr == ".*" {
|
||||
result = append(result, headerExistsChecker{key})
|
||||
result = append(result, headerExistsChecker{strings.TrimSpace(key)})
|
||||
continue
|
||||
}
|
||||
|
||||
rex, err := regexp.Compile(rexStr)
|
||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("while compiling header %s regex %s: %w", key, rexStr, err))
|
||||
continue
|
||||
|
||||
@@ -24,6 +24,7 @@ var (
|
||||
ErrInvalidPathRegex = errors.New("config.Bot: invalid path regex")
|
||||
ErrInvalidHeadersRegex = errors.New("config.Bot: invalid headers regex")
|
||||
ErrInvalidCIDR = errors.New("config.Bot: invalid CIDR")
|
||||
ErrRegexEndsWithNewline = errors.New("config.Bot: regular expression ends with newline (try >- instead of > in yaml)")
|
||||
ErrInvalidImportStatement = errors.New("config.ImportStatement: invalid source file")
|
||||
ErrCantSetBotAndImportValuesAtOnce = errors.New("config.BotOrImport: can't set bot rules and import values at the same time")
|
||||
ErrMustSetBotOrImportRules = errors.New("config.BotOrImport: rule definition is invalid, you must set either bot rules or an import statement, not both")
|
||||
@@ -52,9 +53,11 @@ type BotConfig struct {
|
||||
UserAgentRegex *string `json:"user_agent_regex"`
|
||||
PathRegex *string `json:"path_regex"`
|
||||
HeadersRegex map[string]string `json:"headers_regex"`
|
||||
Action Rule `json:"action"`
|
||||
RemoteAddr []string `json:"remote_addresses"`
|
||||
Challenge *ChallengeRules `json:"challenge,omitempty"`
|
||||
Expression *ExpressionOrList `json:"expression"`
|
||||
|
||||
Action Rule `json:"action"`
|
||||
Challenge *ChallengeRules `json:"challenge,omitempty"`
|
||||
}
|
||||
|
||||
func (b BotConfig) Zero() bool {
|
||||
@@ -82,7 +85,12 @@ func (b BotConfig) Valid() error {
|
||||
errs = append(errs, ErrBotMustHaveName)
|
||||
}
|
||||
|
||||
if b.UserAgentRegex == nil && b.PathRegex == nil && len(b.RemoteAddr) == 0 && len(b.HeadersRegex) == 0 {
|
||||
allFieldsEmpty := b.UserAgentRegex == nil &&
|
||||
b.PathRegex == nil &&
|
||||
len(b.RemoteAddr) == 0 &&
|
||||
len(b.HeadersRegex) == 0
|
||||
|
||||
if allFieldsEmpty && b.Expression == nil {
|
||||
errs = append(errs, ErrBotMustHaveUserAgentOrPath)
|
||||
}
|
||||
|
||||
@@ -91,12 +99,20 @@ func (b BotConfig) Valid() error {
|
||||
}
|
||||
|
||||
if b.UserAgentRegex != nil {
|
||||
if strings.HasSuffix(*b.UserAgentRegex, "\n") {
|
||||
errs = append(errs, fmt.Errorf("%w: user agent regex: %q", ErrRegexEndsWithNewline, *b.UserAgentRegex))
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(*b.UserAgentRegex); err != nil {
|
||||
errs = append(errs, ErrInvalidUserAgentRegex, err)
|
||||
}
|
||||
}
|
||||
|
||||
if b.PathRegex != nil {
|
||||
if strings.HasSuffix(*b.PathRegex, "\n") {
|
||||
errs = append(errs, fmt.Errorf("%w: path regex: %q", ErrRegexEndsWithNewline, *b.PathRegex))
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(*b.PathRegex); err != nil {
|
||||
errs = append(errs, ErrInvalidPathRegex, err)
|
||||
}
|
||||
@@ -108,6 +124,10 @@ func (b BotConfig) Valid() error {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(expr, "\n") {
|
||||
errs = append(errs, fmt.Errorf("%w: header %s regex: %q", ErrRegexEndsWithNewline, name, expr))
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(expr); err != nil {
|
||||
errs = append(errs, ErrInvalidHeadersRegex, err)
|
||||
}
|
||||
@@ -122,6 +142,12 @@ func (b BotConfig) Valid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if b.Expression != nil {
|
||||
if err := b.Expression.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
switch b.Action {
|
||||
case RuleAllow, RuleBenchmark, RuleChallenge, RuleDeny:
|
||||
// okay
|
||||
|
||||
62
lib/policy/config/expressionorlist.go
Normal file
62
lib/policy/config/expressionorlist.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrExpressionOrListMustBeStringOrObject = errors.New("config: this must be a string or an object")
|
||||
ErrExpressionEmpty = errors.New("config: this expression is empty")
|
||||
ErrExpressionCantHaveBoth = errors.New("config: expression block can't contain multiple expression types")
|
||||
)
|
||||
|
||||
type ExpressionOrList struct {
|
||||
Expression string `json:"-"`
|
||||
All []string `json:"all"`
|
||||
Any []string `json:"any"`
|
||||
}
|
||||
|
||||
func (eol ExpressionOrList) Equal(rhs *ExpressionOrList) bool {
|
||||
if eol.Expression != rhs.Expression {
|
||||
return false
|
||||
}
|
||||
|
||||
if !slices.Equal(eol.All, rhs.All) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !slices.Equal(eol.Any, rhs.Any) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (eol *ExpressionOrList) UnmarshalJSON(data []byte) error {
|
||||
switch string(data[0]) {
|
||||
case `"`: // string
|
||||
return json.Unmarshal(data, &eol.Expression)
|
||||
case "{": // object
|
||||
type RawExpressionOrList ExpressionOrList
|
||||
var val RawExpressionOrList
|
||||
if err := json.Unmarshal(data, &val); err != nil {
|
||||
return err
|
||||
}
|
||||
eol.All = val.All
|
||||
eol.Any = val.Any
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrExpressionOrListMustBeStringOrObject
|
||||
}
|
||||
|
||||
func (eol *ExpressionOrList) Valid() error {
|
||||
if len(eol.All) != 0 && len(eol.Any) != 0 {
|
||||
return ErrExpressionCantHaveBoth
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
73
lib/policy/config/expressionorlist_test.go
Normal file
73
lib/policy/config/expressionorlist_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpressionOrListUnmarshal(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
inp string
|
||||
err error
|
||||
validErr error
|
||||
result *ExpressionOrList
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
inp: `"\"User-Agent\" in headers"`,
|
||||
result: &ExpressionOrList{
|
||||
Expression: `"User-Agent" in headers`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "object-and",
|
||||
inp: `{
|
||||
"all": ["\"User-Agent\" in headers"]
|
||||
}`,
|
||||
result: &ExpressionOrList{
|
||||
All: []string{
|
||||
`"User-Agent" in headers`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "object-or",
|
||||
inp: `{
|
||||
"any": ["\"User-Agent\" in headers"]
|
||||
}`,
|
||||
result: &ExpressionOrList{
|
||||
Any: []string{
|
||||
`"User-Agent" in headers`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both-or-and",
|
||||
inp: `{
|
||||
"all": ["\"User-Agent\" in headers"],
|
||||
"any": ["\"User-Agent\" in headers"]
|
||||
}`,
|
||||
validErr: ErrExpressionCantHaveBoth,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var eol ExpressionOrList
|
||||
|
||||
if err := json.Unmarshal([]byte(tt.inp), &eol); !errors.Is(err, tt.err) {
|
||||
t.Errorf("wanted unmarshal error: %v but got: %v", tt.err, err)
|
||||
}
|
||||
|
||||
if tt.result != nil && !eol.Equal(tt.result) {
|
||||
t.Logf("wanted: %#v", tt.result)
|
||||
t.Logf("got: %#v", &eol)
|
||||
t.Fatal("parsed expression is not what was expected")
|
||||
}
|
||||
|
||||
if err := eol.Valid(); !errors.Is(err, tt.validErr) {
|
||||
t.Errorf("wanted validation error: %v but got: %v", tt.err, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
17
lib/policy/config/testdata/bad/multiple_expression_types.json
vendored
Normal file
17
lib/policy/config/testdata/bad/multiple_expression_types.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "multiple-expression-types",
|
||||
"action": "ALLOW",
|
||||
"expression": {
|
||||
"all": [
|
||||
"userAgent.startsWith(\"git/\") || userAgent.contains(\"libgit\")",
|
||||
"\"Git-Protocol\" in headers && headers[\"Git-Protocol\"] == \"version=2\"\n"
|
||||
],
|
||||
"any": [
|
||||
"userAgent.startsWith(\"evilbot/\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
10
lib/policy/config/testdata/bad/multiple_expression_types.yaml
vendored
Normal file
10
lib/policy/config/testdata/bad/multiple_expression_types.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
bots:
|
||||
- name: multiple-expression-types
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.startsWith("git/") || userAgent.contains("libgit")
|
||||
- >
|
||||
"Git-Protocol" in headers && headers["Git-Protocol"] == "version=2"
|
||||
any:
|
||||
- userAgent.startsWith("evilbot/")
|
||||
21
lib/policy/config/testdata/bad/regex_ends_newline.json
vendored
Normal file
21
lib/policy/config/testdata/bad/regex_ends_newline.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "user-agent-ends-newline",
|
||||
"user_agent_regex": "Mozilla\n",
|
||||
"action": "CHALLENGE"
|
||||
},
|
||||
{
|
||||
"name": "path-ends-newline",
|
||||
"path_regex": "^/evil/.*$\n",
|
||||
"action": "CHALLENGE"
|
||||
},
|
||||
{
|
||||
"name": "headers-ends-newline",
|
||||
"headers_regex": {
|
||||
"CF-Worker": ".*\n"
|
||||
},
|
||||
"action": "CHALLENGE"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
lib/policy/config/testdata/bad/regex_ends_newline.yaml
vendored
Normal file
17
lib/policy/config/testdata/bad/regex_ends_newline.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
bots:
|
||||
- name: user-agent-ends-newline
|
||||
# Subtle bug: this ends with a newline
|
||||
user_agent_regex: >
|
||||
Mozilla
|
||||
action: CHALLENGE
|
||||
- name: path-ends-newline
|
||||
# Subtle bug: this ends with a newline
|
||||
path_regex: >
|
||||
^/evil/.*$
|
||||
action: CHALLENGE
|
||||
- name: headers-ends-newline
|
||||
# Subtle bug: this ends with a newline
|
||||
headers_regex:
|
||||
CF-Worker: >
|
||||
.*
|
||||
action: CHALLENGE
|
||||
14
lib/policy/config/testdata/good/git_client.json
vendored
Normal file
14
lib/policy/config/testdata/good/git_client.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "allow-git-clients",
|
||||
"action": "ALLOW",
|
||||
"expression": {
|
||||
"all": [
|
||||
"userAgent.startsWith(\"git/\") || userAgent.contains(\"libgit\")",
|
||||
"\"Git-Protocol\" in headers && headers[\"Git-Protocol\"] == \"version=2\""
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
8
lib/policy/config/testdata/good/git_client.yaml
vendored
Normal file
8
lib/policy/config/testdata/good/git_client.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
bots:
|
||||
- name: allow-git-clients
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.startsWith("git/") || userAgent.contains("libgit")
|
||||
- >
|
||||
"Git-Protocol" in headers && headers["Git-Protocol"] == "version=2"
|
||||
79
lib/policy/config/testdata/good/old_xesite.json
vendored
Normal file
79
lib/policy/config/testdata/good/old_xesite.json
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "amazonbot",
|
||||
"user_agent_regex": "Amazonbot",
|
||||
"action": "DENY"
|
||||
},
|
||||
{
|
||||
"name": "googlebot",
|
||||
"user_agent_regex": "\\+http\\:\\/\\/www\\.google\\.com/bot\\.html",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "bingbot",
|
||||
"user_agent_regex": "\\+http\\:\\/\\/www\\.bing\\.com/bingbot\\.htm",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "qwantbot",
|
||||
"user_agent_regex": "\\+https\\:\\/\\/help\\.qwant\\.com/bot/",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "discordbot",
|
||||
"user_agent_regex": "Discordbot/2\\.0; \\+https\\:\\/\\/discordapp\\.com",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "blueskybot",
|
||||
"user_agent_regex": "Bluesky Cardyb",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "us-artificial-intelligence-scraper",
|
||||
"user_agent_regex": "\\+https\\:\\/\\/github\\.com\\/US-Artificial-Intelligence\\/scraper",
|
||||
"action": "DENY"
|
||||
},
|
||||
{
|
||||
"name": "well-known",
|
||||
"path_regex": "^/.well-known/.*$",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "favicon",
|
||||
"path_regex": "^/favicon.ico$",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "robots-txt",
|
||||
"path_regex": "^/robots.txt$",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "rss-readers",
|
||||
"path_regex": ".*\\.(rss|xml|atom|json)$",
|
||||
"action": "ALLOW"
|
||||
},
|
||||
{
|
||||
"name": "lightpanda",
|
||||
"user_agent_regex": "^Lightpanda/.*$",
|
||||
"action": "DENY"
|
||||
},
|
||||
{
|
||||
"name": "headless-chrome",
|
||||
"user_agent_regex": "HeadlessChrome",
|
||||
"action": "DENY"
|
||||
},
|
||||
{
|
||||
"name": "headless-chromium",
|
||||
"user_agent_regex": "HeadlessChromium",
|
||||
"action": "DENY"
|
||||
},
|
||||
{
|
||||
"name": "generic-browser",
|
||||
"user_agent_regex": "Mozilla",
|
||||
"action": "CHALLENGE"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
lib/policy/expressions/README.md
Normal file
3
lib/policy/expressions/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Expressions support
|
||||
|
||||
The expressions support is based on ideas from [go-away](https://git.gammaspectra.live/git/go-away) but with different opinions about how things should be done.
|
||||
81
lib/policy/expressions/environment.go
Normal file
81
lib/policy/expressions/environment.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/ext"
|
||||
)
|
||||
|
||||
// NewEnvironment creates a new CEL environment, this is the set of
|
||||
// variables and functions that are passed into the CEL scope so that
|
||||
// Anubis can fail loudly and early when something is invalid instead
|
||||
// of blowing up at runtime.
|
||||
func NewEnvironment() (*cel.Env, error) {
|
||||
return cel.NewEnv(
|
||||
ext.Strings(
|
||||
ext.StringsLocale("en_US"),
|
||||
ext.StringsValidateFormatCalls(true),
|
||||
),
|
||||
|
||||
// default all timestamps to UTC
|
||||
cel.DefaultUTCTimeZone(true),
|
||||
|
||||
// Variables exposed to CEL programs:
|
||||
|
||||
// Request metadata
|
||||
cel.Variable("remoteAddress", cel.StringType),
|
||||
cel.Variable("host", cel.StringType),
|
||||
cel.Variable("method", cel.StringType),
|
||||
cel.Variable("userAgent", cel.StringType),
|
||||
cel.Variable("path", cel.StringType),
|
||||
cel.Variable("query", cel.MapType(cel.StringType, cel.StringType)),
|
||||
cel.Variable("headers", cel.MapType(cel.StringType, cel.StringType)),
|
||||
|
||||
// System load metadata
|
||||
cel.Variable("load_1m", cel.DoubleType),
|
||||
cel.Variable("load_5m", cel.DoubleType),
|
||||
cel.Variable("load_15m", cel.DoubleType),
|
||||
|
||||
// Functions exposed to CEL programs:
|
||||
|
||||
// userAgent.isBrowserLike() method, used to detect if a user agent is likely a browser
|
||||
// based on shibboleth words in the User-Agent string.
|
||||
cel.Function("isBrowserLike",
|
||||
cel.MemberOverload("userAgent_isBrowserLike_string",
|
||||
[]*cel.Type{cel.StringType},
|
||||
cel.BoolType,
|
||||
cel.UnaryBinding(func(userAgentVal ref.Val) ref.Val {
|
||||
var userAgent string
|
||||
switch v := userAgentVal.Value().(type) {
|
||||
case string:
|
||||
userAgent = v
|
||||
default:
|
||||
return types.NewErr("invalid type %T", userAgentVal)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(userAgent, "Mozilla"), strings.Contains(userAgent, "Opera"), strings.Contains(userAgent, "Gecko"), strings.Contains(userAgent, "WebKit"), strings.Contains(userAgent, "Apple"), strings.Contains(userAgent, "Chrome"), strings.Contains(userAgent, "Windows"), strings.Contains(userAgent, "Linux"):
|
||||
return types.Bool(true)
|
||||
default:
|
||||
return types.Bool(false)
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Compile takes CEL environment and syntax tree then emits an optimized
|
||||
// Program for execution.
|
||||
func Compile(env *cel.Env, ast *cel.Ast) (cel.Program, error) {
|
||||
return env.Program(
|
||||
ast,
|
||||
cel.EvalOptions(
|
||||
// optimize regular expressions right now instead of on the fly
|
||||
cel.OptOptimize,
|
||||
),
|
||||
)
|
||||
}
|
||||
75
lib/policy/expressions/http_headers.go
Normal file
75
lib/policy/expressions/http_headers.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/common/types/traits"
|
||||
)
|
||||
|
||||
// HTTPHeaders is a type wrapper to expose HTTP headers into CEL programs.
|
||||
type HTTPHeaders struct {
|
||||
http.Header
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
switch typeVal {
|
||||
case types.MapType:
|
||||
return h
|
||||
case types.TypeType:
|
||||
return types.MapType
|
||||
}
|
||||
|
||||
return types.NewErr("can't convert from %q to %q", types.MapType, typeVal)
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) Equal(other ref.Val) ref.Val {
|
||||
return types.Bool(false) // We don't want to compare header maps
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) Type() ref.Type {
|
||||
return types.MapType
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) Value() any { return h }
|
||||
|
||||
func (h HTTPHeaders) Find(key ref.Val) (ref.Val, bool) {
|
||||
k, ok := key.(types.String)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if _, ok := h.Header[string(k)]; !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return types.String(strings.Join(h.Header.Values(string(k)), ",")), true
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) Contains(key ref.Val) ref.Val {
|
||||
_, ok := h.Find(key)
|
||||
return types.Bool(ok)
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) Get(key ref.Val) ref.Val {
|
||||
result, ok := h.Find(key)
|
||||
if !ok {
|
||||
return types.ValOrErr(result, "no such key: %v", key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) Iterator() traits.Iterator { panic("TODO(Xe): implement me") }
|
||||
|
||||
func (h HTTPHeaders) IsZeroValue() bool {
|
||||
return len(h.Header) == 0
|
||||
}
|
||||
|
||||
func (h HTTPHeaders) Size() ref.Val { return types.Int(len(h.Header)) }
|
||||
52
lib/policy/expressions/http_headers_test.go
Normal file
52
lib/policy/expressions/http_headers_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
func TestHTTPHeaders(t *testing.T) {
|
||||
headers := HTTPHeaders{
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/json"},
|
||||
"Cf-Worker": {"true"},
|
||||
"User-Agent": {"Go-http-client/2"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("contains-existing-header", func(t *testing.T) {
|
||||
resp := headers.Contains(types.String("User-Agent"))
|
||||
if !bool(resp.(types.Bool)) {
|
||||
t.Fatal("headers does not contain User-Agent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not-contains-missing-header", func(t *testing.T) {
|
||||
resp := headers.Contains(types.String("Xxx-Random-Header"))
|
||||
if bool(resp.(types.Bool)) {
|
||||
t.Fatal("headers does not contain User-Agent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-existing-header", func(t *testing.T) {
|
||||
val := headers.Get(types.String("User-Agent"))
|
||||
switch val.(type) {
|
||||
case types.String:
|
||||
// ok
|
||||
default:
|
||||
t.Fatalf("result was wrong type %T", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not-get-missing-header", func(t *testing.T) {
|
||||
val := headers.Get(types.String("Xxx-Random-Header"))
|
||||
switch val.(type) {
|
||||
case *types.Err:
|
||||
// ok
|
||||
default:
|
||||
t.Fatalf("result was wrong type %T", val)
|
||||
}
|
||||
})
|
||||
}
|
||||
104
lib/policy/expressions/join.go
Normal file
104
lib/policy/expressions/join.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
)
|
||||
|
||||
// JoinOperator is a type wrapper for and/or operators.
|
||||
//
|
||||
// This is a separate type so that validation can be done at the type level.
|
||||
type JoinOperator string
|
||||
|
||||
// Possible values for JoinOperator
|
||||
const (
|
||||
JoinAnd JoinOperator = "&&"
|
||||
JoinOr JoinOperator = "||"
|
||||
)
|
||||
|
||||
// Valid ensures that JoinOperator is semantically valid.
|
||||
func (jo JoinOperator) Valid() error {
|
||||
switch jo {
|
||||
case JoinAnd, JoinOr:
|
||||
return nil
|
||||
default:
|
||||
return ErrWrongJoinOperator
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrWrongJoinOperator = errors.New("expressions: invalid join operator")
|
||||
ErrNoExpressions = errors.New("expressions: cannot join zero expressions")
|
||||
ErrCantCompile = errors.New("expressions: can't compile one expression")
|
||||
)
|
||||
|
||||
// JoinClauses joins a list of compiled clauses into one big if statement.
|
||||
//
|
||||
// Imagine the following two clauses:
|
||||
//
|
||||
// ball.color == "red"
|
||||
// ball.shape == "round"
|
||||
//
|
||||
// JoinClauses would emit one "joined" clause such as:
|
||||
//
|
||||
// ( ball.color == "red" ) && ( ball.shape == "round" )
|
||||
func JoinClauses(env *cel.Env, operator JoinOperator, clauses ...*cel.Ast) (*cel.Ast, error) {
|
||||
if err := operator.Valid(); err != nil {
|
||||
return nil, fmt.Errorf("%w: wanted && or ||, got: %q", err, operator)
|
||||
}
|
||||
|
||||
switch len(clauses) {
|
||||
case 0:
|
||||
return nil, ErrNoExpressions
|
||||
case 1:
|
||||
return clauses[0], nil
|
||||
}
|
||||
|
||||
var exprs []string
|
||||
var errs []error
|
||||
|
||||
for _, clause := range clauses {
|
||||
clauseStr, err := cel.AstToString(clause)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
exprs = append(exprs, "( "+clauseStr+" )")
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, fmt.Errorf("errors while decompiling statements: %w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
statement := strings.Join(exprs, " "+string(operator)+" ")
|
||||
result, iss := env.Compile(statement)
|
||||
if iss != nil {
|
||||
return nil, iss.Err()
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func Join(env *cel.Env, operator JoinOperator, clauses ...string) (*cel.Ast, error) {
|
||||
var statements []*cel.Ast
|
||||
var errs []error
|
||||
|
||||
for _, clause := range clauses {
|
||||
stmt, iss := env.Compile(clause)
|
||||
if iss != nil && iss.Err() != nil {
|
||||
errs = append(errs, fmt.Errorf("%w: %q gave: %w", ErrCantCompile, clause, iss.Err()))
|
||||
continue
|
||||
}
|
||||
statements = append(statements, stmt)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, fmt.Errorf("errors while joining clauses: %w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
return JoinClauses(env, operator, statements...)
|
||||
}
|
||||
90
lib/policy/expressions/join_test.go
Normal file
90
lib/policy/expressions/join_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
)
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
env, err := NewEnvironment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
clauses []string
|
||||
op JoinOperator
|
||||
err error
|
||||
resultStr string
|
||||
}{
|
||||
{
|
||||
name: "no-clauses",
|
||||
clauses: []string{},
|
||||
op: JoinAnd,
|
||||
err: ErrNoExpressions,
|
||||
},
|
||||
{
|
||||
name: "one-clause-identity",
|
||||
clauses: []string{`remoteAddress == "8.8.8.8"`},
|
||||
op: JoinAnd,
|
||||
err: nil,
|
||||
resultStr: `remoteAddress == "8.8.8.8"`,
|
||||
},
|
||||
{
|
||||
name: "multi-clause-and",
|
||||
clauses: []string{
|
||||
`remoteAddress == "8.8.8.8"`,
|
||||
`host == "anubis.techaro.lol"`,
|
||||
},
|
||||
op: JoinAnd,
|
||||
err: nil,
|
||||
resultStr: `remoteAddress == "8.8.8.8" && host == "anubis.techaro.lol"`,
|
||||
},
|
||||
{
|
||||
name: "multi-clause-or",
|
||||
clauses: []string{
|
||||
`remoteAddress == "8.8.8.8"`,
|
||||
`host == "anubis.techaro.lol"`,
|
||||
},
|
||||
op: JoinOr,
|
||||
err: nil,
|
||||
resultStr: `remoteAddress == "8.8.8.8" || host == "anubis.techaro.lol"`,
|
||||
},
|
||||
{
|
||||
name: "git-user-agent",
|
||||
clauses: []string{
|
||||
`userAgent.startsWith("git/") || userAgent.contains("libgit")`,
|
||||
`"Git-Protocol" in headers && headers["Git-Protocol"] == "version=2"`,
|
||||
},
|
||||
op: JoinAnd,
|
||||
err: nil,
|
||||
resultStr: `(userAgent.startsWith("git/") || userAgent.contains("libgit")) && "Git-Protocol" in headers &&
|
||||
headers["Git-Protocol"] == "version=2"`,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Join(env, tt.op, tt.clauses...)
|
||||
if !errors.Is(err, tt.err) {
|
||||
t.Errorf("wanted error %v but got: %v", tt.err, err)
|
||||
}
|
||||
|
||||
if tt.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
program, err := cel.AstToString(result)
|
||||
if err != nil {
|
||||
t.Fatalf("can't decompile program: %v", err)
|
||||
}
|
||||
|
||||
if tt.resultStr != program {
|
||||
t.Logf("wanted: %s", tt.resultStr)
|
||||
t.Logf("got: %s", program)
|
||||
t.Error("program did not compile as expected")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
67
lib/policy/expressions/loadavg.go
Normal file
67
lib/policy/expressions/loadavg.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/load"
|
||||
)
|
||||
|
||||
type loadAvg struct {
|
||||
lock sync.RWMutex
|
||||
data *load.AvgStat
|
||||
}
|
||||
|
||||
func (l *loadAvg) updateThread(ctx context.Context) {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
l.update()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loadAvg) update() {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
var err error
|
||||
l.data, err = load.Avg()
|
||||
if err != nil {
|
||||
slog.Debug("can't get load average", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
globalLoadAvg *loadAvg
|
||||
)
|
||||
|
||||
func init() {
|
||||
globalLoadAvg = &loadAvg{}
|
||||
go globalLoadAvg.updateThread(context.Background())
|
||||
}
|
||||
|
||||
func Load1() float64 {
|
||||
globalLoadAvg.lock.RLock()
|
||||
defer globalLoadAvg.lock.RUnlock()
|
||||
return globalLoadAvg.data.Load1
|
||||
}
|
||||
|
||||
func Load5() float64 {
|
||||
globalLoadAvg.lock.RLock()
|
||||
defer globalLoadAvg.lock.RUnlock()
|
||||
return globalLoadAvg.data.Load5
|
||||
}
|
||||
|
||||
func Load15() float64 {
|
||||
globalLoadAvg.lock.RLock()
|
||||
defer globalLoadAvg.lock.RUnlock()
|
||||
return globalLoadAvg.data.Load15
|
||||
}
|
||||
78
lib/policy/expressions/url_values.go
Normal file
78
lib/policy/expressions/url_values.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/common/types/traits"
|
||||
)
|
||||
|
||||
var ErrNotImplemented = errors.New("expressions: not implemented")
|
||||
|
||||
// URLValues is a type wrapper to expose url.Values into CEL programs.
|
||||
type URLValues struct {
|
||||
url.Values
|
||||
}
|
||||
|
||||
func (u URLValues) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (u URLValues) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
switch typeVal {
|
||||
case types.MapType:
|
||||
return u
|
||||
case types.TypeType:
|
||||
return types.MapType
|
||||
}
|
||||
|
||||
return types.NewErr("can't convert from %q to %q", types.MapType, typeVal)
|
||||
}
|
||||
|
||||
func (u URLValues) Equal(other ref.Val) ref.Val {
|
||||
return types.Bool(false) // We don't want to compare header maps
|
||||
}
|
||||
|
||||
func (u URLValues) Type() ref.Type {
|
||||
return types.MapType
|
||||
}
|
||||
|
||||
func (u URLValues) Value() any { return u }
|
||||
|
||||
func (u URLValues) Find(key ref.Val) (ref.Val, bool) {
|
||||
k, ok := key.(types.String)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if _, ok := u.Values[string(k)]; !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return types.String(strings.Join(u.Values[string(k)], ",")), true
|
||||
}
|
||||
|
||||
func (u URLValues) Contains(key ref.Val) ref.Val {
|
||||
_, ok := u.Find(key)
|
||||
return types.Bool(ok)
|
||||
}
|
||||
|
||||
func (u URLValues) Get(key ref.Val) ref.Val {
|
||||
result, ok := u.Find(key)
|
||||
if !ok {
|
||||
return types.ValOrErr(result, "no such key: %v", key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (u URLValues) Iterator() traits.Iterator { panic("TODO(Xe): implement me") }
|
||||
|
||||
func (u URLValues) IsZeroValue() bool {
|
||||
return len(u.Values) == 0
|
||||
}
|
||||
|
||||
func (u URLValues) Size() ref.Val { return types.Int(len(u.Values)) }
|
||||
50
lib/policy/expressions/url_values_test.go
Normal file
50
lib/policy/expressions/url_values_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
func TestURLValues(t *testing.T) {
|
||||
headers := URLValues{
|
||||
Values: url.Values{
|
||||
"format": {"json"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("contains-existing-key", func(t *testing.T) {
|
||||
resp := headers.Contains(types.String("format"))
|
||||
if !bool(resp.(types.Bool)) {
|
||||
t.Fatal("headers does not contain User-Agent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not-contains-missing-key", func(t *testing.T) {
|
||||
resp := headers.Contains(types.String("not-there"))
|
||||
if bool(resp.(types.Bool)) {
|
||||
t.Fatal("headers does not contain User-Agent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-existing-key", func(t *testing.T) {
|
||||
val := headers.Get(types.String("format"))
|
||||
switch val.(type) {
|
||||
case types.String:
|
||||
// ok
|
||||
default:
|
||||
t.Fatalf("result was wrong type %T", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not-get-missing-key", func(t *testing.T) {
|
||||
val := headers.Get(types.String("not-there"))
|
||||
switch val.(type) {
|
||||
case *types.Err:
|
||||
// ok
|
||||
default:
|
||||
t.Fatalf("result was wrong type %T", val)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -92,6 +92,15 @@ func ParseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedCon
|
||||
}
|
||||
}
|
||||
|
||||
if b.Expression != nil {
|
||||
c, err := NewCELChecker(b.Expression)
|
||||
if err != nil {
|
||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
||||
} else {
|
||||
cl = append(cl, c)
|
||||
}
|
||||
}
|
||||
|
||||
if b.Challenge == nil {
|
||||
parsedBot.Challenge = &config.ChallengeRules{
|
||||
Difficulty: defaultDifficulty,
|
||||
|
||||
4
lib/testdata/cloudflare-workers-cel.yaml
vendored
Normal file
4
lib/testdata/cloudflare-workers-cel.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
bots:
|
||||
- name: cloudflare-workers
|
||||
expression: '"Cf-Worker" in headers'
|
||||
action: DENY
|
||||
5
lib/testdata/cloudflare-workers-header.yaml
vendored
Normal file
5
lib/testdata/cloudflare-workers-header.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
bots:
|
||||
- name: cloudflare-workers
|
||||
headers_regex:
|
||||
CF-Worker: .*
|
||||
action: DENY
|
||||
212
package-lock.json
generated
212
package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.0.0-see-VERSION-file",
|
||||
"version": "1.17.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.0.0-see-VERSION-file",
|
||||
"version": "1.17.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"cssnano": "^7.0.6",
|
||||
"cssnano-preset-advanced": "^7.0.6",
|
||||
"esbuild": "^0.25.2",
|
||||
"esbuild": "^0.25.3",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
@@ -19,9 +19,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
|
||||
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
|
||||
"integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -36,9 +36,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
|
||||
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
|
||||
"integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -53,9 +53,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
|
||||
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -70,9 +70,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -87,9 +87,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
|
||||
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -104,9 +104,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -121,9 +121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
|
||||
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -138,9 +138,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -155,9 +155,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
|
||||
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
|
||||
"integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -172,9 +172,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
|
||||
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -189,9 +189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
|
||||
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
|
||||
"integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -206,9 +206,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
|
||||
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
|
||||
"integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -223,9 +223,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
|
||||
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
|
||||
"integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -240,9 +240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
|
||||
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
|
||||
"integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -257,9 +257,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
|
||||
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
|
||||
"integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -274,9 +274,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
|
||||
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
|
||||
"integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -291,9 +291,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -308,9 +308,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
|
||||
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -325,9 +325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -342,9 +342,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
|
||||
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -359,9 +359,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -376,9 +376,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -393,9 +393,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
|
||||
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -410,9 +410,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
|
||||
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
|
||||
"integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -427,9 +427,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
|
||||
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1044,9 +1044,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
|
||||
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
|
||||
"integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1057,31 +1057,31 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.2",
|
||||
"@esbuild/android-arm": "0.25.2",
|
||||
"@esbuild/android-arm64": "0.25.2",
|
||||
"@esbuild/android-x64": "0.25.2",
|
||||
"@esbuild/darwin-arm64": "0.25.2",
|
||||
"@esbuild/darwin-x64": "0.25.2",
|
||||
"@esbuild/freebsd-arm64": "0.25.2",
|
||||
"@esbuild/freebsd-x64": "0.25.2",
|
||||
"@esbuild/linux-arm": "0.25.2",
|
||||
"@esbuild/linux-arm64": "0.25.2",
|
||||
"@esbuild/linux-ia32": "0.25.2",
|
||||
"@esbuild/linux-loong64": "0.25.2",
|
||||
"@esbuild/linux-mips64el": "0.25.2",
|
||||
"@esbuild/linux-ppc64": "0.25.2",
|
||||
"@esbuild/linux-riscv64": "0.25.2",
|
||||
"@esbuild/linux-s390x": "0.25.2",
|
||||
"@esbuild/linux-x64": "0.25.2",
|
||||
"@esbuild/netbsd-arm64": "0.25.2",
|
||||
"@esbuild/netbsd-x64": "0.25.2",
|
||||
"@esbuild/openbsd-arm64": "0.25.2",
|
||||
"@esbuild/openbsd-x64": "0.25.2",
|
||||
"@esbuild/sunos-x64": "0.25.2",
|
||||
"@esbuild/win32-arm64": "0.25.2",
|
||||
"@esbuild/win32-ia32": "0.25.2",
|
||||
"@esbuild/win32-x64": "0.25.2"
|
||||
"@esbuild/aix-ppc64": "0.25.3",
|
||||
"@esbuild/android-arm": "0.25.3",
|
||||
"@esbuild/android-arm64": "0.25.3",
|
||||
"@esbuild/android-x64": "0.25.3",
|
||||
"@esbuild/darwin-arm64": "0.25.3",
|
||||
"@esbuild/darwin-x64": "0.25.3",
|
||||
"@esbuild/freebsd-arm64": "0.25.3",
|
||||
"@esbuild/freebsd-x64": "0.25.3",
|
||||
"@esbuild/linux-arm": "0.25.3",
|
||||
"@esbuild/linux-arm64": "0.25.3",
|
||||
"@esbuild/linux-ia32": "0.25.3",
|
||||
"@esbuild/linux-loong64": "0.25.3",
|
||||
"@esbuild/linux-mips64el": "0.25.3",
|
||||
"@esbuild/linux-ppc64": "0.25.3",
|
||||
"@esbuild/linux-riscv64": "0.25.3",
|
||||
"@esbuild/linux-s390x": "0.25.3",
|
||||
"@esbuild/linux-x64": "0.25.3",
|
||||
"@esbuild/netbsd-arm64": "0.25.3",
|
||||
"@esbuild/netbsd-x64": "0.25.3",
|
||||
"@esbuild/openbsd-arm64": "0.25.3",
|
||||
"@esbuild/openbsd-x64": "0.25.3",
|
||||
"@esbuild/sunos-x64": "0.25.3",
|
||||
"@esbuild/win32-arm64": "0.25.3",
|
||||
"@esbuild/win32-ia32": "0.25.3",
|
||||
"@esbuild/win32-x64": "0.25.3"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.0.0-see-VERSION-file",
|
||||
"version": "1.17.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"cssnano": "^7.0.6",
|
||||
"cssnano-preset-advanced": "^7.0.6",
|
||||
"esbuild": "^0.25.2",
|
||||
"esbuild": "^0.25.3",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
|
||||
2
test/anubis_configs/less_paranoid.yaml
Normal file
2
test/anubis_configs/less_paranoid.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
bots:
|
||||
- import: (data)/common/challenge-browser-like.yaml
|
||||
15
test/apache/Dockerfile
Normal file
15
test/apache/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM httpd:2.4
|
||||
|
||||
RUN sed -i \
|
||||
-e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' \
|
||||
-e 's/^#\(LoadModule .*mod_rewrite.so\)/\1/' \
|
||||
-e 's/^#\(LoadModule .*mod_proxy.so\)/\1/' \
|
||||
-e 's/^#\(LoadModule .*mod_proxy_http.so\)/\1/' \
|
||||
-e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' \
|
||||
-e 's/^#\(LoadModule .*mod_http2.so\)/\1/' \
|
||||
conf/httpd.conf
|
||||
RUN echo '' >> conf/httpd.conf \
|
||||
&& echo 'IncludeOptional conf.d/*.conf' >> conf/httpd.conf
|
||||
|
||||
COPY conf.d ./conf.d
|
||||
COPY snippets /etc/httpd/snippets
|
||||
15
test/apache/conf.d/http.conf
Normal file
15
test/apache/conf.d/http.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin your@email.here
|
||||
ServerName httpd.local.cetacean.club
|
||||
DocumentRoot /var/www/httpd.local.cetacean.club
|
||||
|
||||
Include /etc/httpd/snippets/proxy-headers.conf
|
||||
|
||||
ProxyPreserveHost On
|
||||
|
||||
ProxyRequests Off
|
||||
ProxyVia Off
|
||||
|
||||
ProxyPass / http://httpdebug:3000/
|
||||
ProxyPassReverse / http://httpdebug:3000/
|
||||
</VirtualHost>
|
||||
22
test/apache/conf.d/https.conf
Normal file
22
test/apache/conf.d/https.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin me@xeiaso.net
|
||||
ServerName httpd.local.cetacean.club
|
||||
DocumentRoot /var/www/httpd.local.cetacean.club
|
||||
Protocols h2 http/1.1
|
||||
|
||||
SSLCertificateFile /etc/techaro/pki/httpd.local.cetacean.club/cert.pem
|
||||
SSLCertificateKeyFile /etc/techaro/pki/httpd.local.cetacean.club/key.pem
|
||||
Include /etc/httpd/snippets/options-ssl-apache.conf
|
||||
|
||||
Include /etc/httpd/snippets/proxy-headers.conf
|
||||
|
||||
ProxyPreserveHost On
|
||||
|
||||
ProxyRequests Off
|
||||
ProxyVia Off
|
||||
|
||||
ProxyPass / http://anubis:3000
|
||||
ProxyPassReverse / http://anubis:3000
|
||||
</VirtualHost>
|
||||
</IfModule>
|
||||
1
test/apache/conf.d/listen-443-https.conf
Normal file
1
test/apache/conf.d/listen-443-https.conf
Normal file
@@ -0,0 +1 @@
|
||||
Listen 443 https
|
||||
23
test/apache/docker-compose.yaml
Normal file
23
test/apache/docker-compose.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
httpd:
|
||||
image: xxxtest/httpd
|
||||
build: .
|
||||
volumes:
|
||||
- "../shared/www:/var/www/httpd.local.cetacean.club"
|
||||
- "../pki/httpd.local.cetacean.club:/etc/techaro/pki/httpd.local.cetacean.club/"
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
|
||||
anubis:
|
||||
image: git.xeserv.us/techaro/anubis:cel
|
||||
environment:
|
||||
BIND: ":3000"
|
||||
TARGET: http://httpdebug:3000
|
||||
POLICY_FNAME: /etc/techaro/anubis/less_paranoid.yaml
|
||||
volumes:
|
||||
- ../anubis_configs:/etc/techaro/anubis
|
||||
|
||||
httpdebug:
|
||||
image: ghcr.io/xe/x/httpdebug
|
||||
pull_policy: always
|
||||
13
test/apache/snippets/options-ssl-apache.conf
Normal file
13
test/apache/snippets/options-ssl-apache.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
SSLEngine on
|
||||
|
||||
# Intermediate configuration, tweak to your needs
|
||||
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
|
||||
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||
SSLHonorCipherOrder off
|
||||
SSLSessionTickets off
|
||||
|
||||
SSLOptions +StrictRequire
|
||||
|
||||
# Add vhost name to log entries:
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
|
||||
LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
|
||||
3
test/apache/snippets/proxy-headers.conf
Normal file
3
test/apache/snippets/proxy-headers.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR}
|
||||
RequestHeader set "X-Forwarded-Proto" "https"
|
||||
RequestHeader set "X-Http-Version" "%{SERVER_PROTOCOL}s"
|
||||
22
test/apache/start.sh
Executable file
22
test/apache/start.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# If the transient local TLS certificate doesn't exist, mint a new one
|
||||
if [ ! -f ../pki/httpd.local.cetacean.club/cert.pem ]; then
|
||||
# Subshell to contain the directory change
|
||||
(
|
||||
cd ../pki \
|
||||
&& mkdir -p httpd.local.cetacean.club \
|
||||
&& \
|
||||
# Try using https://github.com/FiloSottile/mkcert for better DevEx,
|
||||
# but fall back to using https://github.com/jsha/minica in case
|
||||
# you don't have that installed.
|
||||
(
|
||||
mkcert \
|
||||
--cert-file ./httpd.local.cetacean.club/cert.pem \
|
||||
--key-file ./httpd.local.cetacean.club/key.pem httpd.local.cetacean.club \
|
||||
|| go tool minica -domains httpd.local.cetacean.club
|
||||
)
|
||||
)
|
||||
fi
|
||||
|
||||
docker compose up --build
|
||||
16
test/caddy/Caddyfile
Normal file
16
test/caddy/Caddyfile
Normal file
@@ -0,0 +1,16 @@
|
||||
:80 {
|
||||
reverse_proxy http://anubis:3000 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
header_up X-Http-Version {http.request.proto}
|
||||
}
|
||||
}
|
||||
|
||||
:443 {
|
||||
tls /etc/techaro/pki/caddy.local.cetacean.club/cert.pem /etc/techaro/pki/caddy.local.cetacean.club/key.pem
|
||||
|
||||
reverse_proxy http://anubis:3000 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
header_up X-Http-Version {http.request.proto}
|
||||
header_up X-Tls-Version {http.request.tls.version}
|
||||
}
|
||||
}
|
||||
9
test/caddy/Dockerfile
Normal file
9
test/caddy/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
# FROM caddy:2.10.0-builder AS builder
|
||||
|
||||
# RUN xcaddy build \
|
||||
# --with github.com/lolPants/caddy-requestid
|
||||
|
||||
FROM caddy:2.10.0 AS run
|
||||
|
||||
# COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
22
test/caddy/docker-compose.yaml
Normal file
22
test/caddy/docker-compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
caddy:
|
||||
image: xxxtest/caddy
|
||||
build: .
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
volumes:
|
||||
- "../pki/caddy.local.cetacean.club:/etc/techaro/pki/caddy.local.cetacean.club/"
|
||||
|
||||
anubis:
|
||||
image: git.xeserv.us/techaro/anubis:cel
|
||||
environment:
|
||||
BIND: ":3000"
|
||||
TARGET: http://httpdebug:3000
|
||||
POLICY_FNAME: /etc/techaro/anubis/less_paranoid.yaml
|
||||
volumes:
|
||||
- ../anubis_configs:/etc/techaro/anubis
|
||||
|
||||
httpdebug:
|
||||
image: ghcr.io/xe/x/httpdebug
|
||||
pull_policy: always
|
||||
22
test/caddy/start.sh
Executable file
22
test/caddy/start.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# If the transient local TLS certificate doesn't exist, mint a new one
|
||||
if [ ! -f ../pki/caddy.local.cetacean.club/cert.pem ]; then
|
||||
# Subshell to contain the directory change
|
||||
(
|
||||
cd ../pki \
|
||||
&& mkdir -p caddy.local.cetacean.club \
|
||||
&& \
|
||||
# Try using https://github.com/FiloSottile/mkcert for better DevEx,
|
||||
# but fall back to using https://github.com/jsha/minica in case
|
||||
# you don't have that installed.
|
||||
(
|
||||
mkcert \
|
||||
--cert-file ./caddy.local.cetacean.club/cert.pem \
|
||||
--key-file ./caddy.local.cetacean.club/key.pem caddy.local.cetacean.club \
|
||||
|| go tool minica -domains caddy.local.cetacean.club
|
||||
)
|
||||
)
|
||||
fi
|
||||
|
||||
docker compose up --build
|
||||
4
test/nginx/Dockerfile
Normal file
4
test/nginx/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM nginx
|
||||
|
||||
COPY conf.d/ /etc/nginx/conf.d/
|
||||
COPY snippets /etc/nginx/snippets
|
||||
10
test/nginx/conf.d/http.conf
Normal file
10
test/nginx/conf.d/http.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name nginx.local.cetacean.club;
|
||||
|
||||
location / {
|
||||
proxy_pass http://anubis:3000;
|
||||
include snippets/proxy_params;
|
||||
}
|
||||
}
|
||||
14
test/nginx/conf.d/https.conf
Normal file
14
test/nginx/conf.d/https.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name nginx.local.cetacean.club;
|
||||
|
||||
ssl_certificate /etc/techaro/pki/nginx.local.cetacean.club/cert.pem;
|
||||
ssl_certificate_key /etc/techaro/pki/nginx.local.cetacean.club/key.pem;
|
||||
include snippets/ssl_params;
|
||||
|
||||
location / {
|
||||
proxy_pass http://anubis:3000;
|
||||
include snippets/proxy_params;
|
||||
}
|
||||
}
|
||||
22
test/nginx/docker-compose.yaml
Normal file
22
test/nginx/docker-compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
httpd:
|
||||
image: xxxtest/nginx
|
||||
build: .
|
||||
volumes:
|
||||
- "../pki/nginx.local.cetacean.club:/etc/techaro/pki/nginx.local.cetacean.club/"
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
|
||||
anubis:
|
||||
image: git.xeserv.us/techaro/anubis:cel
|
||||
environment:
|
||||
BIND: ":3000"
|
||||
TARGET: http://httpdebug:3000
|
||||
POLICY_FNAME: /etc/techaro/anubis/less_paranoid.yaml
|
||||
volumes:
|
||||
- ../anubis_configs:/etc/techaro/anubis
|
||||
|
||||
httpdebug:
|
||||
image: ghcr.io/xe/x/httpdebug
|
||||
pull_policy: always
|
||||
7
test/nginx/snippets/proxy_params
Normal file
7
test/nginx/snippets/proxy_params
Normal file
@@ -0,0 +1,7 @@
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Http-Version $server_protocol;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
11
test/nginx/snippets/ssl_params
Normal file
11
test/nginx/snippets/ssl_params
Normal file
@@ -0,0 +1,11 @@
|
||||
ssl_protocols TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
22
test/nginx/start.sh
Executable file
22
test/nginx/start.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# If the transient local TLS certificate doesn't exist, mint a new one
|
||||
if [ ! -f ../pki/nginx.local.cetacean.club/cert.pem ]; then
|
||||
# Subshell to contain the directory change
|
||||
(
|
||||
cd ../pki \
|
||||
&& mkdir -p nginx.local.cetacean.club \
|
||||
&& \
|
||||
# Try using https://github.com/FiloSottile/mkcert for better DevEx,
|
||||
# but fall back to using https://github.com/jsha/minica in case
|
||||
# you don't have that installed.
|
||||
(
|
||||
mkcert \
|
||||
--cert-file ./nginx.local.cetacean.club/cert.pem \
|
||||
--key-file ./nginx.local.cetacean.club/key.pem nginx.local.cetacean.club \
|
||||
|| go tool minica -domains nginx.local.cetacean.club
|
||||
)
|
||||
)
|
||||
fi
|
||||
|
||||
docker compose up --build
|
||||
@@ -1,9 +1,11 @@
|
||||
User-agent: AI2Bot
|
||||
User-agent: Ai2Bot-Dolma
|
||||
User-agent: aiHitBot
|
||||
User-agent: Amazonbot
|
||||
User-agent: anthropic-ai
|
||||
User-agent: Applebot
|
||||
User-agent: Applebot-Extended
|
||||
User-agent: Brightbot 1.0
|
||||
User-agent: Bytespider
|
||||
User-agent: CCBot
|
||||
User-agent: ChatGPT-User
|
||||
@@ -11,9 +13,13 @@ User-agent: Claude-Web
|
||||
User-agent: ClaudeBot
|
||||
User-agent: cohere-ai
|
||||
User-agent: cohere-training-data-crawler
|
||||
User-agent: Cotoyogi
|
||||
User-agent: Crawlspace
|
||||
User-agent: Diffbot
|
||||
User-agent: DuckAssistBot
|
||||
User-agent: FacebookBot
|
||||
User-agent: Factset_spyderbot
|
||||
User-agent: FirecrawlAgent
|
||||
User-agent: FriendlyCrawler
|
||||
User-agent: Google-Extended
|
||||
User-agent: GoogleOther
|
||||
@@ -24,19 +30,27 @@ User-agent: iaskspider/2.0
|
||||
User-agent: ICC-Crawler
|
||||
User-agent: ImagesiftBot
|
||||
User-agent: img2dataset
|
||||
User-agent: imgproxy
|
||||
User-agent: ISSCyberRiskCrawler
|
||||
User-agent: Kangaroo Bot
|
||||
User-agent: meta-externalagent
|
||||
User-agent: Meta-ExternalAgent
|
||||
User-agent: meta-externalfetcher
|
||||
User-agent: Meta-ExternalFetcher
|
||||
User-agent: NovaAct
|
||||
User-agent: OAI-SearchBot
|
||||
User-agent: omgili
|
||||
User-agent: omgilibot
|
||||
User-agent: Operator
|
||||
User-agent: PanguBot
|
||||
User-agent: Perplexity-User
|
||||
User-agent: PerplexityBot
|
||||
User-agent: PetalBot
|
||||
User-agent: Scrapy
|
||||
User-agent: SemrushBot
|
||||
User-agent: SemrushBot-OCOB
|
||||
User-agent: SemrushBot-SWA
|
||||
User-agent: Sidetrade indexer bot
|
||||
User-agent: TikTokSpider
|
||||
User-agent: Timpibot
|
||||
User-agent: VelenPublicWebCrawler
|
||||
User-agent: Webzio-Extended
|
||||
@@ -44,4 +58,4 @@ User-agent: YouBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
Disallow: /
|
||||
|
||||
Reference in New Issue
Block a user