Compare commits

...

20 Commits

Author SHA1 Message Date
Xe Iaso
b6c361c294 fix(default-config): isBrowser -> isBrowserLike
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 18:07:36 -04:00
Xe Iaso
e9e602976f docs(admin/configuration): add expressions docs outline
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 18:03:10 -04:00
Xe Iaso
33bb5803a8 docs(admin/environments): update apache/nginx docs
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 17:56:28 -04:00
Xe Iaso
ada7b3a179 docs(admin): add guide for making Anubis far less aggressive by default
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 17:56:06 -04:00
Xe Iaso
dfa7025afe test: add apache, caddy, and nginx smoke test environments
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 17:52:23 -04:00
Xe Iaso
884af5fd4c fix(data/common): use new isBrowserLike function for less aggressive challenge rule
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 17:51:58 -04:00
Xe Iaso
3fb8fa2009 feat(policy/expressions): add userAgent.isBrowserLike method
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 16:15:05 -04:00
Xe Iaso
b43df36f7d feat(policy): add system load average as checker input
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 15:44:12 -04:00
Xe Iaso
fd058964fa feat(data/apps): add API route allow rule for non-HEAD/GET
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 13:06:24 -04:00
Xe Iaso
fb20b36b18 feat(data/bots): add two example IRC bots
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 00:46:59 -04:00
Xe Iaso
84cba05167 fix(data): enable legacy rule for now, admins will have to opt-in
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 00:41:05 -04:00
Xe Iaso
9f988578a4 feat(data): add challenge-browser-like.yaml
This is a huge change to Anubis and will make it a lot more invisible at
the cost of requiring additional server configuration to make it happen.

If you add this bit of nginx config to your location block:

```nginx
proxy_set_header X-Http-Version $server_protocol;
```

And then adjust the bottom bot rule to this:

```yaml
- import: (data)/common/challenge-browser-like.yaml
```

Anubis will be way less aggressive than it was before. This will let
through any traffic that comes from a browser that actually is a browser
via some more advanced heuristics.

I think that this rule alone is the key feature of v1.18.0.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-28 00:20:27 -04:00
Xe Iaso
ea4e5751ab test(lib): ensure that Cf-Worker via expression works
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:50:24 -04:00
Xe Iaso
4e1db3842e fix(policy/expressions): do not Contains missing keys
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:49:44 -04:00
Xe Iaso
029c79ba28 fix(lib/test): fix failing test and invalid cloudflare workers rule
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:25:50 -04:00
Xe Iaso
9f8ede7fe3 data/clients: fix curl-impersonate definition
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:25:50 -04:00
Xe Iaso
80bd7c563b chore(data): reformat some things for expressions
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:25:49 -04:00
Xe Iaso
92a3e5ba81 fix: rename and/or to all/any
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:25:49 -04:00
Xe Iaso
65cbc6922c data: add some examples of expression-based rules
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:25:49 -04:00
Xe Iaso
eae3a7b5e4 feat(lib/policy): add support for CEL checkers
This adds the ability for administrators to use Common Expression
Language[0] (CEL) for more advanced check logic than Anubis previously
offered.

These can be as simple as:

```yaml
- name: allow-api-routes
  action: ALLOW
  expression:
    and:
    - '!(method == "HEAD" || method == "GET")'
    - path.startsWith("/api/")
```

or get as complicated as:

```yaml
- name: allow-git-clients
  action: ALLOW
  expression:
    and:
    - 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"
```

Internally these are compiled and evaluated with cel-go[1]. This also
leaves room for extensibility should that be desired in the future. This
will intersect with #338 and eventually intersect with TLS fingerprints
as in #337.

[0]: https://cel.dev/
[1]: https://github.com/google/cel-go

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-04-27 22:25:49 -04:00
63 changed files with 1580 additions and 22 deletions

View File

@@ -0,0 +1,6 @@
- name: allow-api-routes
action: ALLOW
expression:
all:
- '!(method == "HEAD" || method == "GET")'
- path.startsWith("/api/")

View File

@@ -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"
},
@@ -46,4 +52,4 @@
}
],
"dnsbl": false
}
}

View File

@@ -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

View 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")

View File

@@ -1,4 +1,3 @@
- name: cloudflare-workers
headers_regex:
CF-Worker: .*
action: DENY
expression: '"Cf-Worker" in headers'
action: CHALLENGE

View 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"

View 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"

View 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
View 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
View 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"

View File

@@ -0,0 +1,6 @@
- name: allow-api-routes
action: ALLOW
expression:
all:
- '!(method == "HEAD" || method == "GET")'
- path.startsWith("/api/")

View 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()

View File

@@ -7,4 +7,7 @@
action: ALLOW
- name: robots-txt
path_regex: ^/robots.txt$
action: ALLOW
- name: sitemap
path_regex: ^/sitemap.xml$
action: ALLOW

View File

@@ -0,0 +1,4 @@
# Generic catchall rule
- name: generic-browser
expression: userAgent.isBrowserLike()
action: CHALLENGE

View File

@@ -0,0 +1,3 @@
- name: no-user-agent-string
expression: userAgent == ""
action: DENY

View File

@@ -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
)

View 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)

View File

@@ -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

View File

@@ -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;
}

View 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.

18
go.mod
View File

@@ -6,46 +6,58 @@ 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.13.0 // indirect
golang.org/x/sys v0.32.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
)

42
go.sum
View File

@@ -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,12 +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=
@@ -124,18 +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=

View File

@@ -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)
}
})
})
}
}

114
lib/policy/celchecker.go Normal file
View 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
}
}

View File

@@ -53,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 {
@@ -83,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)
}
@@ -135,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

View 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
}

View 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)
}
})
}
}

View 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/\")"
]
}
}
]
}

View 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/")

View 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\""
]
}
}
]
}

View 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"

View 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.

View 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,
),
)
}

View 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)) }

View 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)
}
})
}

View 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...)
}

View 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")
}
})
}
}

View 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
}

View 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)) }

View 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)
}
})
}

View File

@@ -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,

View File

@@ -0,0 +1,4 @@
bots:
- name: cloudflare-workers
expression: '"Cf-Worker" in headers'
action: DENY

View File

@@ -0,0 +1,5 @@
bots:
- name: cloudflare-workers
headers_regex:
CF-Worker: .*
action: DENY

View File

@@ -0,0 +1,2 @@
bots:
- import: (data)/common/challenge-browser-like.yaml

15
test/apache/Dockerfile Normal file
View 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

View 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>

View 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>

View File

@@ -0,0 +1 @@
Listen 443 https

View 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

View 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

View 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
View 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
View 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
View 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

View 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
View 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
View File

@@ -0,0 +1,4 @@
FROM nginx
COPY conf.d/ /etc/nginx/conf.d/
COPY snippets /etc/nginx/snippets

View 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;
}
}

View 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;
}
}

View 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

View 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;

View 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
View 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