mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-06 00:38:18 +00:00
Compare commits
42 Commits
Xe/express
...
v1.18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c7640aa09 | ||
|
|
b1c276db9f | ||
|
|
7b84904d15 | ||
|
|
7f0f691ba5 | ||
|
|
1c6c07939a | ||
|
|
c633b3349e | ||
|
|
2e54e839f1 | ||
|
|
3701b2bc3d | ||
|
|
6200c4c123 | ||
|
|
16412a8bf9 | ||
|
|
2e9b18a510 | ||
|
|
e64987ef90 | ||
|
|
8ff28fbb33 | ||
|
|
e953b514fa | ||
|
|
52a6a65cc4 | ||
|
|
99f645a590 | ||
|
|
3b50b4c6c0 | ||
|
|
8ee0529321 | ||
|
|
799f47efbf | ||
|
|
865d513e35 | ||
|
|
af07691139 | ||
|
|
74dcebf20b | ||
|
|
92d3dd361b | ||
|
|
9e760b1c16 | ||
|
|
fc54e95208 | ||
|
|
f879e0d307 | ||
|
|
6e82373718 | ||
|
|
f8e1000ab0 | ||
|
|
fa362c8ec9 | ||
|
|
76f2029fb5 | ||
|
|
5d9cc40e34 | ||
|
|
63b8411220 | ||
|
|
803aa35d66 | ||
|
|
cb523333a1 | ||
|
|
91275c489f | ||
|
|
feb3dd2bcb | ||
|
|
06a762959f | ||
|
|
74d330cec5 | ||
|
|
2935bd4aa7 | ||
|
|
7d52e9ff5e | ||
|
|
4184b42282 | ||
|
|
7a20a46b0d |
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
SLOG_LEVEL: debug
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
|
||||
with:
|
||||
subject-name: ghcr.io/techarohq/anubis
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
18
.github/workflows/go.yml
vendored
18
.github/workflows/go.yml
vendored
@@ -66,15 +66,14 @@ jobs:
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: install playwright browsers
|
||||
run: |
|
||||
npx --yes playwright@1.51.1 install --with-deps
|
||||
npx --yes playwright@1.51.1 run-server --port 9001 &
|
||||
|
||||
- name: install node deps
|
||||
run: |
|
||||
npm ci
|
||||
npm run assets
|
||||
|
||||
- name: install playwright browsers
|
||||
run: |
|
||||
npx --no-install playwright@1.52.0 install --with-deps
|
||||
npx --no-install playwright@1.52.0 run-server --port 9001 &
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
@@ -82,6 +81,11 @@ jobs:
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- uses: dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6 # v1.3.1
|
||||
- name: Lint with staticcheck
|
||||
uses: dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6 # v1.3.1
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Govulncheck
|
||||
run: |
|
||||
go tool govulncheck ./...
|
||||
|
||||
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.2.1/yeet_0.2.1_amd64.deb -O var/yeet.deb
|
||||
wget https://github.com/TecharoHQ/yeet/releases/download/v0.2.2/yeet_0.2.2_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.2.1/yeet_0.2.1_amd64.deb -O var/yeet.deb
|
||||
wget https://github.com/TecharoHQ/yeet/releases/download/v0.2.2/yeet_0.2.2_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@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
|
||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
|
||||
|
||||
- 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@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
5
.github/zizmor.yml
vendored
Normal file
5
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
policies:
|
||||
Homebrew/actions/*: any
|
||||
15
.vscode/settings.json
vendored
Normal file
15
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"github.copilot.enable": {
|
||||
"*": false,
|
||||
"plaintext": false,
|
||||
"markdown": false,
|
||||
"mdx": false,
|
||||
"json": false,
|
||||
"scminput": false,
|
||||
"yaml": false,
|
||||
"go": false,
|
||||
"zig": false,
|
||||
"javascript": false,
|
||||
"properties": false
|
||||
}
|
||||
}
|
||||
1
Makefile
1
Makefile
@@ -23,6 +23,7 @@ build: assets
|
||||
lint: assets
|
||||
$(GO) vet ./...
|
||||
$(GO) tool staticcheck ./...
|
||||
$(GO) tool govulncheck ./...
|
||||
|
||||
prebaked-build:
|
||||
$(GO) build -o ./var/anubis -ldflags "-X 'github.com/TecharoHQ/anubis.Version=$(VERSION)'" ./cmd/anubis
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
Anubis is brought to you by sponsors and donors like:
|
||||
|
||||
[](https://distrust.co)
|
||||
[](https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh)
|
||||
[](https://canine.tools)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Package anubis contains the version number of Anubis.
|
||||
package anubis
|
||||
|
||||
import "time"
|
||||
|
||||
// Version is the current version of Anubis.
|
||||
//
|
||||
// This variable is set at build time using the -X linker flag. If not set,
|
||||
@@ -11,6 +13,9 @@ var Version = "devel"
|
||||
// access.
|
||||
const CookieName = "within.website-x-cmd-anubis-auth"
|
||||
|
||||
// CookieDefaultExpirationTime is the amount of time before the cookie/JWT expires.
|
||||
const CookieDefaultExpirationTime = 7 * 24 * time.Hour
|
||||
|
||||
// BasePrefix is a global prefix for all Anubis endpoints. Can be emptied to remove the prefix entirely.
|
||||
var BasePrefix = ""
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ var (
|
||||
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
||||
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
||||
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
|
||||
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
|
||||
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
||||
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
||||
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
||||
@@ -59,6 +60,7 @@ var (
|
||||
debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate")
|
||||
ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough")
|
||||
ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time")
|
||||
ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache")
|
||||
extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
|
||||
webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
|
||||
)
|
||||
@@ -205,16 +207,15 @@ func main() {
|
||||
log.Fatalf("can't parse policy file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Rule error IDs:")
|
||||
ruleErrorIDs := make(map[string]string)
|
||||
for _, rule := range policy.Bots {
|
||||
if rule.Action != config.RuleDeny {
|
||||
continue
|
||||
}
|
||||
|
||||
hash := rule.Hash()
|
||||
fmt.Printf("* %s: %s\n", rule.Name, hash)
|
||||
ruleErrorIDs[rule.Name] = hash
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// replace the bot policy rules with a single rule that always benchmarks
|
||||
if *debugBenchmarkJS {
|
||||
@@ -272,18 +273,20 @@ func main() {
|
||||
}
|
||||
|
||||
s, err := libanubis.New(libanubis.Options{
|
||||
BasePrefix: *basePrefix,
|
||||
Next: rp,
|
||||
Policy: policy,
|
||||
ServeRobotsTXT: *robotsTxt,
|
||||
PrivateKey: priv,
|
||||
CookieDomain: *cookieDomain,
|
||||
CookiePartitioned: *cookiePartitioned,
|
||||
OGPassthrough: *ogPassthrough,
|
||||
OGTimeToLive: *ogTimeToLive,
|
||||
RedirectDomains: redirectDomainsList,
|
||||
Target: *target,
|
||||
WebmasterEmail: *webmasterEmail,
|
||||
BasePrefix: *basePrefix,
|
||||
Next: rp,
|
||||
Policy: policy,
|
||||
ServeRobotsTXT: *robotsTxt,
|
||||
PrivateKey: priv,
|
||||
CookieDomain: *cookieDomain,
|
||||
CookieExpiration: *cookieExpiration,
|
||||
CookiePartitioned: *cookiePartitioned,
|
||||
OGPassthrough: *ogPassthrough,
|
||||
OGTimeToLive: *ogTimeToLive,
|
||||
RedirectDomains: redirectDomainsList,
|
||||
Target: *target,
|
||||
WebmasterEmail: *webmasterEmail,
|
||||
OGCacheConsidersHost: *ogCacheConsiderHost,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
@@ -306,7 +309,7 @@ func main() {
|
||||
h = internal.XForwardedForToXRealIP(h)
|
||||
h = internal.XForwardedForUpdate(h)
|
||||
|
||||
srv := http.Server{Handler: h}
|
||||
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
||||
slog.Info(
|
||||
"listening",
|
||||
@@ -320,6 +323,8 @@ func main() {
|
||||
"og-passthrough", *ogPassthrough,
|
||||
"og-expiry-time", *ogTimeToLive,
|
||||
"base-prefix", *basePrefix,
|
||||
"cookie-expiration-time", *cookieExpiration,
|
||||
"rule-error-ids", ruleErrorIDs,
|
||||
)
|
||||
|
||||
go func() {
|
||||
@@ -343,7 +348,7 @@ func metricsServer(ctx context.Context, done func()) {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(anubis.BasePrefix+"/metrics", promhttp.Handler())
|
||||
|
||||
srv := http.Server{Handler: mux}
|
||||
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
|
||||
slog.Debug("listening for metrics", "url", metricsUrl)
|
||||
|
||||
|
||||
@@ -1,47 +1,17 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"import": "(data)/bots/_deny-pathological.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/bots/ai-robots-txt.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/bots/cloudflare-workers.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/bots/headless-browsers.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/bots/us-ai-scraper.yaml"
|
||||
"import": "(data)/crawlers/_allow-good.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/bots/aggressive-brazilian-scrapers.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/clients/curl-impersonate.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/googlebot.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/bingbot.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/duckduckbot.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/qwantbot.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/internet-archive.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/kagibot.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/marginalia.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/crawlers/mojeekbot.yaml"
|
||||
},
|
||||
{
|
||||
"import": "(data)/common/keep-internet-working.yaml"
|
||||
},
|
||||
@@ -51,5 +21,9 @@
|
||||
"action": "CHALLENGE"
|
||||
}
|
||||
],
|
||||
"dnsbl": false
|
||||
}
|
||||
"dnsbl": false,
|
||||
"status_codes": {
|
||||
"CHALLENGE": 200,
|
||||
"DENY": 200
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,24 @@
|
||||
|
||||
bots:
|
||||
# Pathological bots to deny
|
||||
- # This correlates to data/bots/ai-robots-txt.yaml in the source tree
|
||||
import: (data)/bots/ai-robots-txt.yaml
|
||||
- import: (data)/bots/cloudflare-workers.yaml
|
||||
- import: (data)/bots/headless-browsers.yaml
|
||||
- import: (data)/bots/us-ai-scraper.yaml
|
||||
- # This correlates to data/bots/deny-pathological.yaml in the source tree
|
||||
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/deny-pathological.yaml
|
||||
import: (data)/bots/_deny-pathological.yaml
|
||||
- import: (data)/bots/aggressive-brazilian-scrapers.yaml
|
||||
- import: (data)/clients/curl-impersonate.yaml
|
||||
|
||||
# Search engines to allow
|
||||
- import: (data)/crawlers/googlebot.yaml
|
||||
- import: (data)/crawlers/bingbot.yaml
|
||||
- import: (data)/crawlers/duckduckbot.yaml
|
||||
- import: (data)/crawlers/qwantbot.yaml
|
||||
- import: (data)/crawlers/internet-archive.yaml
|
||||
- import: (data)/crawlers/kagibot.yaml
|
||||
- import: (data)/crawlers/marginalia.yaml
|
||||
- import: (data)/crawlers/mojeekbot.yaml
|
||||
# Enforce https://github.com/ai-robots-txt/ai.robots.txt
|
||||
- import: (data)/bots/ai-robots-txt.yaml
|
||||
|
||||
# Search engine crawlers to allow, defaults to:
|
||||
# - Google (so they don't try to bypass Anubis)
|
||||
# - Bing
|
||||
# - DuckDuckGo
|
||||
# - Qwant
|
||||
# - The Internet Archive
|
||||
# - Kagi
|
||||
# - Marginalia
|
||||
# - Mojeek
|
||||
- import: (data)/crawlers/_allow-good.yaml
|
||||
|
||||
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
|
||||
- import: (data)/common/keep-internet-working.yaml
|
||||
@@ -43,11 +44,18 @@ bots:
|
||||
# report_as: 4 # lie to the operator
|
||||
# algorithm: slow # intentionally waste CPU cycles and time
|
||||
|
||||
# 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()
|
||||
# Generic catchall rule
|
||||
- name: generic-browser
|
||||
user_agent_regex: >-
|
||||
Mozilla|Opera
|
||||
action: CHALLENGE
|
||||
|
||||
dnsbl: false
|
||||
|
||||
# By default, send HTTP 200 back to clients that either get issued a challenge
|
||||
# or a denial. This seems weird, but this is load-bearing due to the fact that
|
||||
# the most aggressive scraper bots seem to really, really, want an HTTP 200 and
|
||||
# will stop sending requests once they get it.
|
||||
status_codes:
|
||||
CHALLENGE: 200
|
||||
DENY: 200
|
||||
3
data/bots/_deny-pathological.yaml
Normal file
3
data/bots/_deny-pathological.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
- import: (data)/bots/cloudflare-workers.yaml
|
||||
- import: (data)/bots/headless-browsers.yaml
|
||||
- import: (data)/bots/us-ai-scraper.yaml
|
||||
@@ -1,3 +1,4 @@
|
||||
- name: cloudflare-workers
|
||||
expression: '"Cf-Worker" in headers'
|
||||
action: CHALLENGE
|
||||
headers_regex:
|
||||
CF-Worker: .*
|
||||
action: DENY
|
||||
@@ -1,32 +0,0 @@
|
||||
- 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"'
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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
data/common/json-api.yaml
Normal file
7
data/common/json-api.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
- name: allow-api-requests
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- '"Accept" in headers'
|
||||
- 'headers["Accept"] == "application/json"'
|
||||
- 'path.startsWith("/api/")'
|
||||
@@ -1,4 +0,0 @@
|
||||
# Generic catchall rule
|
||||
- name: generic-browser
|
||||
expression: userAgent.isBrowserLike()
|
||||
action: CHALLENGE
|
||||
@@ -1,3 +1,3 @@
|
||||
- name: no-user-agent-string
|
||||
expression: userAgent == ""
|
||||
action: DENY
|
||||
action: DENY
|
||||
expression: userAgent == ""
|
||||
8
data/crawlers/_allow-good.yaml
Normal file
8
data/crawlers/_allow-good.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
- import: (data)/crawlers/googlebot.yaml
|
||||
- import: (data)/crawlers/bingbot.yaml
|
||||
- import: (data)/crawlers/duckduckbot.yaml
|
||||
- import: (data)/crawlers/qwantbot.yaml
|
||||
- import: (data)/crawlers/internet-archive.yaml
|
||||
- import: (data)/crawlers/kagibot.yaml
|
||||
- import: (data)/crawlers/marginalia.yaml
|
||||
- import: (data)/crawlers/mojeekbot.yaml
|
||||
@@ -3,6 +3,6 @@ package data
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed botPolicies.yaml botPolicies.json apps bots clients common crawlers
|
||||
//go:embed botPolicies.yaml botPolicies.json all:apps all:bots all:clients all:common all:crawlers
|
||||
BotPolicies embed.FS
|
||||
)
|
||||
|
||||
@@ -11,6 +11,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## v1.18.0: Varis zos Galvus
|
||||
|
||||
The big ticket feature in this release is [CEL expression matching support](https://anubis.techaro.lol/docs/admin/configuration/expressions). This allows you to tailor your approach for the individual services you are protecting.
|
||||
|
||||
These can be as simple as:
|
||||
|
||||
```yaml
|
||||
- name: allow-api-requests
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- '"Accept" in headers'
|
||||
- 'headers["Accept"] == "application/json"'
|
||||
- 'path.startsWith("/api/")'
|
||||
```
|
||||
|
||||
Or as complicated as:
|
||||
|
||||
```yaml
|
||||
- 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"
|
||||
```
|
||||
|
||||
The docs have more information, but here's a tl;dr of the variables you have access to in expressions:
|
||||
|
||||
| Name | Type | Explanation | Example |
|
||||
| :-------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
|
||||
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
|
||||
| `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. |
|
||||
| `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` |
|
||||
| `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` |
|
||||
| `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` |
|
||||
| `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` |
|
||||
|
||||
This will be made more elaborate in the future. Give me time. This is a [simple, lovable, and complete](https://longform.asmartbear.com/slc/) implementation of this feature so that administrators can get hacking ASAP.
|
||||
|
||||
Other changes:
|
||||
|
||||
- Use CSS variables to deduplicate styles
|
||||
- Fixed native packages not containing the stdlib and botPolicies.yaml
|
||||
- Change import syntax to allow multi-level imports
|
||||
- Changed the startup logging to use JSON formatting as all the other logs do.
|
||||
- Added the ability to do [expression matching with CEL](./admin/configuration/expressions.mdx)
|
||||
- Add a warning for clients that don't store cookies
|
||||
- Disable Open Graph passthrough by default ([#435](https://github.com/TecharoHQ/anubis/issues/435))
|
||||
- Clarify the license of the mascot images ([#442](https://github.com/TecharoHQ/anubis/issues/442))
|
||||
- Started Suppressing 'Context canceled' errors from http in the logs ([#446](https://github.com/TecharoHQ/anubis/issues/446))
|
||||
|
||||
## v1.17.1: Asahi sas Brutus: Echo 1
|
||||
|
||||
- Added customization of authorization cookie expiration time with `--cookie-expiration-time` flag or envvar
|
||||
- Updated the `OG_PASSTHROUGH` to be true by default, thereby allowing Open Graph tags to be passed through by default
|
||||
- Added the ability to [customize Anubis' HTTP status codes](./admin/configuration/custom-status-codes.mdx) ([#355](https://github.com/TecharoHQ/anubis/issues/355))
|
||||
|
||||
## v1.17.0: Asahi sas Brutus
|
||||
|
||||
- Ensure regexes can't end in newlines ([#372](https://github.com/TecharoHQ/anubis/issues/372))
|
||||
@@ -23,7 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added support to allow to restrict the allowed redirect domains
|
||||
- Whitelisted [DuckDuckBot](https://duckduckgo.com/duckduckgo-help-pages/results/duckduckbot/) in botPolicies
|
||||
- Improvements to build scripts to make them less independent of the build host
|
||||
- Improved the OpenGraph error logging
|
||||
- Improved the Open Graph error logging
|
||||
- Added `Opera` to the `generic-browser` bot policy rule
|
||||
- Added FreeBSD rc.d script so can be run as a FreeBSD daemon
|
||||
- Allow requests from the Internet Archive
|
||||
@@ -41,6 +108,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fixed mojeekbot user agent regex
|
||||
- Added support for running anubis behind a base path (e.g. `/myapp`)
|
||||
- Reduce Anubis' paranoia with user cookies ([#365](https://github.com/TecharoHQ/anubis/pull/365))
|
||||
- Added support for Open Graph passthrough while using unix sockets
|
||||
- The Open Graph subsystem now passes the HTTP `HOST` header through to the origin
|
||||
- Updated the `OG_PASSTHROUGH` to be true by default, thereby allowing Open Graph tags to be passed through by default
|
||||
|
||||
## v1.16.0
|
||||
|
||||
@@ -54,7 +124,7 @@ The following features are the "big ticket" items:
|
||||
- A prebaked tarball has been added, allowing distros to build Anubis like they could in v1.15.x
|
||||
- The placeholder Anubis mascot has been replaced with a design by [CELPHASE](https://bsky.app/profile/celphase.bsky.social)
|
||||
- Verification page now shows hash rate and a progress bar for completion probability
|
||||
- Added support for [OpenGraph tags](https://ogp.me/) when rendering the challenge page. This allows for social previews to be generated when sharing the challenge page on social media platforms ([#195](https://github.com/TecharoHQ/anubis/pull/195))
|
||||
- Added support for [Open Graph tags](https://ogp.me/) when rendering the challenge page. This allows for social previews to be generated when sharing the challenge page on social media platforms ([#195](https://github.com/TecharoHQ/anubis/pull/195))
|
||||
- Added support for passing the ed25519 signing key in a file with `-ed25519-private-key-hex-file` or `ED25519_PRIVATE_KEY_HEX_FILE`
|
||||
|
||||
The other small fixes have been made:
|
||||
|
||||
19
docs/docs/admin/configuration/custom-status-codes.mdx
Normal file
19
docs/docs/admin/configuration/custom-status-codes.mdx
Normal file
@@ -0,0 +1,19 @@
|
||||
# Custom status codes for Anubis errors
|
||||
|
||||
Out of the box, Anubis will reply with `HTTP 200` for challenge and denial pages. This is intended to make AI scrapers have a hard time with your website because when they are faced with a non-200 response, they will hammer the page over and over until they get a 200 response. This behavior may not be desirable, as such Anubis lets you customize what HTTP status codes are returned when Anubis throws challenge and denial pages.
|
||||
|
||||
This is configured in the `status_codes` block of your [bot policy file](../policies.mdx):
|
||||
|
||||
```yaml
|
||||
status_codes:
|
||||
CHALLENGE: 200
|
||||
DENY: 200
|
||||
```
|
||||
|
||||
To match CloudFlare's behavior, use a configuration like this:
|
||||
|
||||
```yaml
|
||||
status_codes:
|
||||
CHALLENGE: 403
|
||||
DENY: 403
|
||||
```
|
||||
@@ -1,25 +1,150 @@
|
||||
# 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)
|
||||
Most of the Anubis matchers let you match individual parts of a request and only those parts in isolation. In order to defend a service in depth, you often need the ability to match against multiple aspects of a request. Anubis implements [Common Expression Language (CEL)](https://cel.dev) to let administrators define these more advanced rules. This allows you to tailor your approach for the individual services you are protecting.
|
||||
|
||||
As an example, here is a rule that lets you allow JSON API requests through Anubis:
|
||||
|
||||
```yaml
|
||||
- name: allow-api-requests
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- '"Accept" in headers'
|
||||
- 'headers["Accept"] == "application/json"'
|
||||
- 'path.startsWith("/api/")'
|
||||
```
|
||||
|
||||
This is an advanced feature and as such it is easy to get yourself in trouble with it. Use this with care.
|
||||
|
||||
## Common Expression Language (CEL)
|
||||
|
||||
CEL is an expression language made by Google as a part of their access control lists system. As programs grow more complicated and users have the need to express more complicated security requirements, they often want the ability to just run a small bit of code to check things for themselves. CEL expressions are built for this. They are implicitly sandboxed so that they cannot affect the system they are running in and also designed to evaluate as fast as humanly possible.
|
||||
|
||||
Imagine a CEL expression as the contents of an `if` statement in JavaScript or the `WHERE` clause in SQL. Consider this example expression:
|
||||
|
||||
```python
|
||||
userAgent == ""
|
||||
```
|
||||
|
||||
This is roughly equivalent to the following in JavaScript:
|
||||
|
||||
```js
|
||||
if (userAgent == "") {
|
||||
// Do something
|
||||
}
|
||||
```
|
||||
|
||||
Using these expressions, you can define more elaborate rules as facts and circumstances demand. For more information about the syntax and grammar of CEL, take a look at [the language specification](https://github.com/google/cel-spec/blob/master/doc/langdef.md).
|
||||
|
||||
## How Anubis uses CEL
|
||||
|
||||
Anubis uses CEL to let administrators create complicated filter rules. Anubis has several modes of using CEL:
|
||||
|
||||
- Validating requests against single expressions
|
||||
- Validating multiple expressions and ensuring at least one of them are true (`any`)
|
||||
- Validating multiple expressions and ensuring all of them are true (`all`)
|
||||
|
||||
The common pattern is that every Anubis expression returns `true`, `false`, or raises an error.
|
||||
|
||||
### Single expressions
|
||||
|
||||
A single expression that returns either `true` or `false`. If the expression returns `true`, then the action specified in the rule will be taken. If it returns `false`, Anubis will move on to the next rule.
|
||||
|
||||
For example, consider this rule:
|
||||
|
||||
```yaml
|
||||
- name: no-user-agent-string
|
||||
action: DENY
|
||||
expression: userAgent == ""
|
||||
```
|
||||
|
||||
For this rule, if a request comes in without a [`User-Agent` string](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) set, Anubis will deny the request and return an error page.
|
||||
|
||||
### `any` blocks
|
||||
|
||||
An `any` block that contains a list of expressions. If any expression in the list returns `true`, then the action specified in the rule will be taken. If all expressions in that list return `false`, Anubis will move on to the next rule.
|
||||
|
||||
For example, consider this rule:
|
||||
|
||||
```yaml
|
||||
- name: known-banned-user
|
||||
action: DENY
|
||||
expression:
|
||||
any:
|
||||
- remoteAddress == "8.8.8.8"
|
||||
- remoteAddress == "1.1.1.1"
|
||||
```
|
||||
|
||||
For this rule, if a request comes in from `8.8.8.8` or `1.1.1.1`, Anubis will deny the request and return an error page.
|
||||
|
||||
#### `all` blocks
|
||||
|
||||
An `all` block that contains a list of expressions. If all expressions in the list return `true`, then the action specified in the rule will be taken. If any of the expressions in the list returns `false`, Anubis will move on to the next rule.
|
||||
|
||||
For example, consider this rule:
|
||||
|
||||
```yaml
|
||||
- name: go-get
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.startsWith("Go-http-client/")
|
||||
- '"go-get" in query'
|
||||
- query["go-get"] == "1"
|
||||
```
|
||||
|
||||
For this rule, if a request comes in matching [the signature of the `go get` command](https://pkg.go.dev/cmd/go#hdr-Remote_import_paths), Anubis will allow it through to the target.
|
||||
|
||||
## Variables exposed to Anubis expressions
|
||||
|
||||
Anubis exposes the following variables to expressions:
|
||||
|
||||
| Name | Type | Explanation | Example |
|
||||
| :-------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
|
||||
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
|
||||
| `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. |
|
||||
| `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` |
|
||||
| `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` |
|
||||
| `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` |
|
||||
| `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` |
|
||||
|
||||
Of note: in many languages when you look up a key in a map and there is nothing there, the language will return some "falsy" value like `undefined` in JavaScript, `None` in Python, or the zero value of the type in Go. In CEL, if you try to look up a value that does not exist, execution of the expression will fail and Anubis will return an error.
|
||||
|
||||
In order to avoid this, make sure the header or query parameter you are testing is present in the request with an `all` block like this:
|
||||
|
||||
```yaml
|
||||
- name: challenge-wiki-history-page
|
||||
action: CHALLENGE
|
||||
all:
|
||||
- 'path == "/index.php"'
|
||||
- '"title" in query'
|
||||
- '"action" in query'
|
||||
- 'query["action"] == "history"
|
||||
```
|
||||
|
||||
This rule throws a challenge if and only if all of the following conditions are true:
|
||||
|
||||
- The URL path is `/index.php`
|
||||
- The URL query string contains a `title` value
|
||||
- The URL query string contains an `action` value
|
||||
- The URL query string's `action` value is `"history"`
|
||||
|
||||
So given an HTTP request like this:
|
||||
|
||||
```text
|
||||
GET /index.php?title=Index&action=history HTTP/1.1
|
||||
User-Agent: Mozilla/5.0 Gecko/20100101 Firefox/137.0
|
||||
Host: wiki.int.techaro.lol
|
||||
X-Real-Ip: 8.8.8.8
|
||||
```
|
||||
|
||||
Anubis would return a challenge because all of those conditions are true.
|
||||
|
||||
## Functions exposed to Anubis expressions
|
||||
|
||||
There are currently no functions from the Anubis runtime exposed to expressions. This will change in the future.
|
||||
|
||||
## Life advice
|
||||
|
||||
Expressions are very powerful. This is a benefit and a burden. If you are not careful with your expression targeting, you will be liable to get yourself into trouble. If you are at all in doubt, throw a `CHALLENGE` over a `DENY`. Legitimate users can easily work around a `CHALLENGE` result with a [proof of work challenge](../../design/why-proof-of-work.mdx). Bots are less likely to be able to do this.
|
||||
|
||||
@@ -79,6 +79,45 @@ config.BotOrImport: rule definition is invalid, you must set either bot rules or
|
||||
|
||||
Paths can either be prefixed with `(data)` to import from the [the data folder in the Anubis source tree](https://github.com/TecharoHQ/anubis/tree/main/data) or anywhere on the filesystem. If you don't have access to the Anubis source tree, check /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
|
||||
|
||||
## Importing from imports
|
||||
|
||||
You can also import from an imported file in case you want to import an entire folder of rules at once.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="json" label="JSON">
|
||||
|
||||
```json
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"import": "(data)/bots/_deny-pathological.yaml"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yaml" label="YAML" default>
|
||||
|
||||
```yaml
|
||||
bots:
|
||||
- import: (data)/bots/_deny-pathological.yaml
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
This lets you import an entire ruleset at once:
|
||||
|
||||
```yaml
|
||||
# (data)/bots/_deny-pathological.yaml
|
||||
- import: (data)/bots/cloudflare-workers.yaml
|
||||
- import: (data)/bots/headless-browsers.yaml
|
||||
- import: (data)/bots/us-ai-scraper.yaml
|
||||
```
|
||||
|
||||
Use this with care, you can easily get yourself into a state where Anubis recursively imports things for eternity if you are not careful. The best way to use this is to make a "root import" named `_everything.yaml` or `_allow-good.yaml` so they sort to the top. Name your meta-imports after the main verb they are enforcing so that you can glance at the configuration file and understand what it's doing.
|
||||
|
||||
## Writing snippets
|
||||
|
||||
Snippets can be written in either JSON or YAML, with a preference for YAML. When writing a snippet, write the bot rules you want directly at the top level of the file in a list.
|
||||
|
||||
@@ -5,14 +5,15 @@ title: Open Graph Configuration
|
||||
|
||||
# Open Graph Configuration
|
||||
|
||||
This page provides detailed information on how to configure [OpenGraph tag](https://ogp.me/) passthrough in Anubis. This enables social previews of resources protected by Anubis without having to exempt each scraper individually.
|
||||
This page provides detailed information on how to configure [Open Graph tag](https://ogp.me/) passthrough in Anubis. This enables social previews of resources protected by Anubis without having to exempt each scraper individually.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Name | Description | Type | Default | Example |
|
||||
|------------------|-----------------------------------------------------------|----------|---------|-------------------------|
|
||||
| `OG_PASSTHROUGH` | Enables or disables the Open Graph tag passthrough system | Boolean | `false` | `OG_PASSTHROUGH=true` |
|
||||
| `OG_EXPIRY_TIME` | Configurable cache expiration time for Open Graph tags | Duration | `24h` | `OG_EXPIRY_TIME=1h` |
|
||||
| Name | Description | Type | Default | Example |
|
||||
| ------------------------ | --------------------------------------------------------- | -------- | ------- | ----------------------------- |
|
||||
| `OG_PASSTHROUGH` | Enables or disables the Open Graph tag passthrough system | Boolean | `true` | `OG_PASSTHROUGH=true` |
|
||||
| `OG_EXPIRY_TIME` | Configurable cache expiration time for Open Graph tags | Duration | `24h` | `OG_EXPIRY_TIME=1h` |
|
||||
| `OG_CACHE_CONSIDER_HOST` | Enables or disables the use of the host in the cache key | Boolean | `false` | `OG_CACHE_CONSIDER_HOST=true` |
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -21,6 +22,7 @@ To configure Open Graph tags, you can set the following environment variables, e
|
||||
```sh
|
||||
export OG_PASSTHROUGH=true
|
||||
export OG_EXPIRY_TIME=1h
|
||||
export OG_CACHE_CONSIDER_HOST=false
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
@@ -33,6 +35,8 @@ When `OG_PASSTHROUGH` is enabled, Anubis will:
|
||||
|
||||
The cache expiration time is controlled by `OG_EXPIRY_TIME`.
|
||||
|
||||
When `OG_CACHE_CONSIDER_HOST` is enabled, Anubis will include the host in the cache key for Open Graph tags. This ensures that tags are cached separately for different hosts.
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example of how to configure Open Graph tags in your Anubis setup:
|
||||
@@ -40,8 +44,19 @@ Here is an example of how to configure Open Graph tags in your Anubis setup:
|
||||
```sh
|
||||
export OG_PASSTHROUGH=true
|
||||
export OG_EXPIRY_TIME=1h
|
||||
export OG_CACHE_CONSIDER_HOST=false
|
||||
```
|
||||
|
||||
With these settings, Anubis will cache Open Graph tags for 1 hour and pass them through to the challenge page.
|
||||
With these settings, Anubis will cache Open Graph tags for 1 hour and pass them through to the challenge page, not considering the host in the cache key.
|
||||
|
||||
## When to Enable `OG_CACHE_CONSIDER_HOST`
|
||||
|
||||
In most cases, you would want to keep `OG_CACHE_CONSIDER_HOST` set to `false` to avoid unnecessary cache fragmentation. However, there are some scenarios where enabling this option can be beneficial:
|
||||
|
||||
1. **Multi-Tenant Applications**: If you are running a multi-tenant application where different tenants are hosted on different subdomains, enabling `OG_CACHE_CONSIDER_HOST` ensures that the Open Graph tags are cached separately for each tenant. This prevents one tenant's Open Graph tags from being served to another tenant's users.
|
||||
|
||||
2. **Different Content for Different Hosts**: If your application serves different content based on the host, enabling `OG_CACHE_CONSIDER_HOST` ensures that the correct Open Graph tags are cached and served for each host. This is useful for applications that have different branding or content for different domains or subdomains.
|
||||
|
||||
3. **Security and Privacy Concerns**: In some cases, you may want to ensure that Open Graph tags are not shared between different hosts for security or privacy reasons. Enabling `OG_CACHE_CONSIDER_HOST` ensures that the tags are cached separately for each host, preventing any potential leakage of information between hosts.
|
||||
|
||||
For more information, refer to the [installation guide](../installation).
|
||||
|
||||
@@ -62,6 +62,11 @@ location /.within.website/ {
|
||||
# Assumption: Anubis is running in the same network namespace as
|
||||
# nginx on localhost TCP port 8923
|
||||
proxy_pass http://127.0.0.1:8923;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header content-length "";
|
||||
auth_request off;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,8 +91,7 @@ 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-Http-Version" "%{SERVER_PROTOCOL}s"
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
|
||||
ProxyPreserveHost On
|
||||
|
||||
@@ -120,7 +119,7 @@ Make sure to add a separate configuration file for the listener on port 3001:
|
||||
```text
|
||||
# /etc/httpd/conf.d/listener-3001.conf
|
||||
|
||||
Listen 3001
|
||||
Listen 127.0.0.1:3001
|
||||
```
|
||||
|
||||
This can be repeated for multiple sites. Anubis does not care about the HTTP `Host` header and will happily cope with multiple websites via the same instance.
|
||||
|
||||
71
docs/docs/admin/environments/caddy.mdx
Normal file
71
docs/docs/admin/environments/caddy.mdx
Normal file
@@ -0,0 +1,71 @@
|
||||
# Caddy
|
||||
|
||||
To use Anubis with Caddy, stick Anubis between Caddy and your backend. For example, consider this application setup:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Caddy with Anubis in the middle
|
||||
---
|
||||
|
||||
flowchart LR
|
||||
T(User Traffic)
|
||||
TCP(TCP 80/443)
|
||||
An(Anubis)
|
||||
B(Backend)
|
||||
Blocked
|
||||
|
||||
T --> TCP
|
||||
TCP --> |Traffic filtering| An
|
||||
An --> |Happy traffic| B
|
||||
An --> |Malicious traffic| Blocked
|
||||
```
|
||||
|
||||
Instead of your traffic going directly to your backend, it takes a detour through Anubis. Anubis filters out the "bad" traffic and passes the "good" traffic to the backend.
|
||||
|
||||
To set up Anubis with Docker compose and Caddy, start with a `docker-compose` configuration like this:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 443:443/udp
|
||||
volumes:
|
||||
- ./conf:/etc/caddy
|
||||
- caddy_config:/config
|
||||
- caddy_data:/data
|
||||
|
||||
anubis:
|
||||
image: ghcr.io/techarohq/anubis:latest
|
||||
pull_policy: always
|
||||
environment:
|
||||
BIND: ":3000"
|
||||
TARGET: http://httpdebug:3000
|
||||
|
||||
httpdebug:
|
||||
image: ghcr.io/xe/x/httpdebug
|
||||
pull_policy: always
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
```
|
||||
|
||||
And then put the following in `conf/Caddyfile`:
|
||||
|
||||
```Caddyfile
|
||||
# conf/Caddyfile
|
||||
|
||||
yourdomain.example.com {
|
||||
tls your@email.address
|
||||
|
||||
reverse_proxy http://anubis:3000 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
header_up X-Http-Version {http.request.proto}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want to protect multiple services with Anubis, you will need to either start multiple instances of Anubis (Anubis requires less than 32 MB of ram on average) or set up a two-tier routing setup where TLS termination is done with one instance of Caddy and the actual routing to services is done with another instance of Caddy. See the [nginx](./nginx.mdx) or [Apache](./apache.mdx) documentation to get ideas on how you would do this.
|
||||
@@ -59,14 +59,8 @@ 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;
|
||||
}
|
||||
|
||||
@@ -153,7 +147,7 @@ upstream anubis {
|
||||
|
||||
# Try anubis first over a UNIX socket
|
||||
server unix:/run/anubis/nginx.sock;
|
||||
#server http://127.0.0.1:8923;
|
||||
#server 127.0.0.1:8923;
|
||||
|
||||
# Optional: fall back to serving the websites directly. This allows your
|
||||
# websites to be resilient against Anubis failing, at the risk of exposing
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: traefik
|
||||
title: Integrate Anubis with Traefik in a Docker Compose Environment
|
||||
title: Traefik
|
||||
---
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- traefik.enable=true # Eneabling Traefik
|
||||
- traefik.enable=true # Enabling Traefik
|
||||
- traefik.docker.network=traefik # Telling Traefik which network to use
|
||||
- traefik.http.routers.target2.rule=Host(`another.com`) # Only Matching Requests for example.com
|
||||
- traefik.http.routers.target2.entrypoints=websecure # Listening on the exclusive Anubis Network
|
||||
|
||||
@@ -54,7 +54,8 @@ Anubis uses these environment variables for configuration:
|
||||
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints. For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
|
||||
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
||||
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
||||
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See [here](https://stackoverflow.com/a/1063760) for more information. |
|
||||
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information. |
|
||||
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
||||
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
||||
@@ -63,6 +64,7 @@ Anubis uses these environment variables for configuration:
|
||||
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
||||
| `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. |
|
||||
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. |
|
||||
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. |
|
||||
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
|
||||
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain. |
|
||||
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# 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.
|
||||
@@ -37,7 +37,7 @@ flowchart TD
|
||||
ValidateChallenge -- If anything is wrong --> Fail
|
||||
```
|
||||
|
||||
### Challenge presentation
|
||||
## Challenge presentation
|
||||
|
||||
Anubis decides to present a challenge using this logic:
|
||||
|
||||
@@ -89,7 +89,7 @@ work valid?"}
|
||||
PresentChallenge -- Back again for another cycle --> Request
|
||||
```
|
||||
|
||||
### Proof of passing challenges
|
||||
## Proof of passing challenges
|
||||
|
||||
When a client passes a challenge, Anubis sets an HTTP cookie named `"within.website-x-cmd-anubis-auth"` containing a signed [JWT](https://jwt.io/) (JSON Web Token). This JWT contains the following claims:
|
||||
|
||||
@@ -102,7 +102,7 @@ When a client passes a challenge, Anubis sets an HTTP cookie named `"within.webs
|
||||
|
||||
This ensures that the token has enough metadata to prove that the token is valid (due to the token's signature), but also so that the server can independently prove the token is valid. This cookie is allowed to be set without triggering an EU cookie banner notification; but depending on facts and circumstances, you may wish to disclose this to your users.
|
||||
|
||||
### Challenge format
|
||||
## Challenge format
|
||||
|
||||
Challenges are formed by taking some user request metadata and using that to generate a SHA-256 checksum. The following request headers are used:
|
||||
|
||||
@@ -115,6 +115,6 @@ Challenges are formed by taking some user request metadata and using that to gen
|
||||
|
||||
This forms a fingerprint of the requestor using metadata that any requestor already is sending. It also uses time as an input, which is known to both the server and requestor due to the nature of linear timelines. Depending on facts and circumstances, you may wish to disclose this to your users.
|
||||
|
||||
### JWT signing
|
||||
## JWT signing
|
||||
|
||||
Anubis uses an ed25519 keypair to sign the JWTs issued when challenges are passed. Anubis will generate a new ed25519 keypair every time it starts. At this time, there is no way to share this keypair between instance of Anubis, but that will be addressed in future versions.
|
||||
|
||||
@@ -20,6 +20,8 @@ title: Anubis
|
||||
Anubis is brought to you by sponsors and donors like:
|
||||
|
||||
[](https://distrust.co)
|
||||
[](https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh)
|
||||
[](https://canine.tools)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
20
docs/docs/user/frequently-asked-questions.mdx
Normal file
20
docs/docs/user/frequently-asked-questions.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## Why can't you just put details about the proof of work challenge into the challenge page so I don't need to run JavaScript?
|
||||
|
||||
A common question is something along the lines of "why can't you give me a shell script to run the challenge on my laptop so that I don't have to enable JavaScript". Malware has been known to show an interstitial that [asks the user to paste something into their run box on Windows](https://www.malwarebytes.com/blog/news/2025/03/fake-captcha-websites-hijack-your-clipboard-to-install-information-stealers), which will then make that machine a zombie in a botnet.
|
||||
|
||||
It would be in very bad taste to associate a security product such as Anubis with behavior similar to what malware uses. This would destroy user trust in the product and potentially result in reputational damage for the contributors. When at all possible, we want to avoid this happening.
|
||||
|
||||
Technically inclined users are easily able to understand how the proof of work check works by either reading the JavaScript on the page or [reading the source code of the JavaScript program](https://github.com/TecharoHQ/anubis/tree/main/web/js). Please note that the format of the challenges and the algorithms used to solve them are liable to change without notice and are not considered part of the public API of Anubis. When such a change occurs, this will break your workarounds.
|
||||
|
||||
If [sufficient funding is raised](https://github.com/TecharoHQ/anubis/discussions/278), a browser extension that packages the proof of work checks and looks for Anubis challenge pages to solve them will be created.
|
||||
|
||||
## Why does Anubis use [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) to do its proof of work challenge?
|
||||
|
||||
Anubis uses [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) to do its proof of work challenge for two main reasons:
|
||||
|
||||
1. The proof of work operation is a lot of serially blocking calls. If you do serially blocking calls in JavaScript, some browsers will hang and not respond to user input. This is bad user experience. Using a Web Worker allows the browser to do this computation in the background so your browser will not hang.
|
||||
2. Web Workers allow you to do multithreaded execution of JavaScript code. This lets Anubis run its checks in parallel across all your system cores so that the challenge can complete as fast as possible. In the last decade, most CPU advancements have come from making cores and code extremely parallel. Using Web Workers lets Anubis take advantage of your hardware as much as possible so that the challenge finishes as fast as possible.
|
||||
|
||||
If you use a browser extension such as [JShelter](https://jshelter.org/), you will need to [modify your JShelter configuration](./known-broken-extensions.md#jshelter) to allow Anubis' proof of work computation to complete.
|
||||
@@ -3,17 +3,47 @@ title: List of known browser extensions that can break Anubis
|
||||
---
|
||||
|
||||
This page contains a list of all of the browser extensions that are known to break Anubis' functionality and their associated GitHub issues, along with instructions on how to work around the issue.
|
||||
|
||||
## [JShelter](https://jshelter.org/)
|
||||
|
||||
| Extension | JShelter |
|
||||
| :----------- | :-------------------------------------------- |
|
||||
| Website | [jshelter.org](https://jshelter.org/) |
|
||||
| GitHub issue | https://github.com/TecharoHQ/anubis/issues/25 |
|
||||
| Extension | JShelter |
|
||||
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Website | [jshelter.org](https://jshelter.org/) |
|
||||
| GitHub issue | https://github.com/TecharoHQ/anubis/issues/25 |
|
||||
| Be aware of | [What are Web Workers, and what are the threats that I face?](https://jshelter.org/faq/#what-are-web-workers-and-what-are-the-threats-that-i-face) |
|
||||
|
||||
Workaround steps:
|
||||
### Workaround steps (recommended):
|
||||
|
||||
1. Click on the JShelter badge icon (typically in the toolbar next to your navigation bar; if you cannot locate the icon, see [this question](https://jshelter.org/faq/#can-i-see-a-jshelter-badge-icon-next-to-my-navigation-bar-i-want-to-interact-with-the-extension-easily-and-avoid-going-through-settings)).
|
||||
2. Expand JavaScript Shield settings by clicking on the `Modify` button.
|
||||
3. Click on the `Detail tweaks of JS shield for this site` button.
|
||||
4. Click and drag the `WebWorker` slider to the left until `Remove` is replaced by the `Unprotected`.
|
||||
5. Refresh the page, for example, by clicking on the `Refresh page` button at the top of the JShelter pop up window.
|
||||
6. You might want to restore the Worker settings once you go through the challenge.
|
||||
|
||||
### Workaround steps (alternative if you do not want to dig in JShelter's pop up):
|
||||
|
||||
1. Click on the JShelter badge icon (typically in the toolbar next to your navigation bar; if you cannot locate the icon, see [this question](https://jshelter.org/faq/#can-i-see-a-jshelter-badge-icon-next-to-my-navigation-bar-i-want-to-interact-with-the-extension-easily-and-avoid-going-through-settings)).
|
||||
2. Expand JavaScript Shield settings by clicking on the `Modify` button.
|
||||
3. Choose "Turn JavaScript Shield off"
|
||||
4. Refresh the page, for example, by clicking on the `Refresh page` button at the top of the JShelter pop up window.
|
||||
|
||||
:::note
|
||||
|
||||
Taking these actions will remove all protections of JavaScript Shield for all pages at the visited web site. You might want review and amend your JavaScript shield settings once you go through the challenge based on your operational security model.
|
||||
|
||||
:::
|
||||
|
||||
### Workaround steps (alternative if you do not like JShelter's pop up):
|
||||
|
||||
1. Open JShelter extension settings
|
||||
2. Click on JS Shield details
|
||||
3. Enter in the domain for a website protected by Anubis
|
||||
4. Choose "Turn JavaScript Shield off"
|
||||
5. Hit "Add to list"
|
||||
|
||||
:::note
|
||||
|
||||
Taking these actions will remove all protections of JavaScript Shield for all pages at the visited web site. You might want review and amend your JavaScript shield settings once you go through the challenge based on your operational security model.
|
||||
|
||||
:::
|
||||
|
||||
@@ -14,7 +14,6 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
||||
- https://bugs.winehq.org/
|
||||
- https://svnweb.freebsd.org/
|
||||
- https://trac.ffmpeg.org/
|
||||
- https://git.sr.ht/
|
||||
- https://xeiaso.net/
|
||||
- https://source.puri.sm/
|
||||
- https://git.enlightenment.org/
|
||||
@@ -29,8 +28,28 @@ 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/
|
||||
- https://www.cfaarchive.org/
|
||||
- https://gitlab.freedesktop.org/
|
||||
- <details>
|
||||
<summary>FreeCAD</summary>
|
||||
- https://forum.freecad.org/
|
||||
- https://wiki.freecad.org/
|
||||
</details>
|
||||
- <details>
|
||||
<summary>ScummVM</summary>
|
||||
- https://forums.scummvm.org/
|
||||
- https://wiki.scummvm.org/
|
||||
</details>
|
||||
- <details>
|
||||
<summary>Sourceware</summary>
|
||||
- https://sourceware.org/cgit
|
||||
- https://sourceware.org/glibc/wiki
|
||||
- https://builder.sourceware.org/testruns/
|
||||
- https://patchwork.sourceware.org/
|
||||
- https://gcc.gnu.org/bugzilla/
|
||||
- https://gcc.gnu.org/cgit
|
||||
</details>
|
||||
- <details>
|
||||
<summary>The United Nations</summary>
|
||||
- https://policytoolbox.iiep.unesco.org/
|
||||
|
||||
BIN
docs/static/img/sponsors/caninetools-logo.webp
vendored
Normal file
BIN
docs/static/img/sponsors/caninetools-logo.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
docs/static/img/sponsors/terminal-trove.webp
vendored
Normal file
BIN
docs/static/img/sponsors/terminal-trove.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
28
go.mod
28
go.mod
@@ -1,19 +1,20 @@
|
||||
module github.com/TecharoHQ/anubis
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.857
|
||||
github.com/a-h/templ v0.3.865
|
||||
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/playwright-community/playwright-go v0.5200.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
|
||||
golang.org/x/net v0.40.0
|
||||
k8s.io/apimachinery v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -26,34 +27,32 @@ require (
|
||||
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/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.7.0 // 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/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/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
golang.org/x/vuln v1.1.4 // 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
|
||||
@@ -66,5 +65,6 @@ tool (
|
||||
github.com/a-h/templ/cmd/templ
|
||||
golang.org/x/tools/cmd/goimports
|
||||
golang.org/x/tools/cmd/stringer
|
||||
golang.org/x/vuln/cmd/govulncheck
|
||||
honnef.co/go/tools/cmd/staticcheck
|
||||
)
|
||||
|
||||
63
go.sum
63
go.sum
@@ -4,8 +4,8 @@ github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/k
|
||||
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=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
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/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A=
|
||||
github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
||||
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=
|
||||
@@ -20,12 +20,10 @@ 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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
|
||||
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
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=
|
||||
@@ -40,17 +38,19 @@ 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-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
|
||||
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/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
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=
|
||||
@@ -70,13 +70,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||
github.com/playwright-community/playwright-go v0.5101.0 h1:gVCMZThDO76LJ/aCI27lpB8hEAWhZszeS0YB+oTxJp0=
|
||||
github.com/playwright-community/playwright-go v0.5101.0/go.mod h1:kBNWs/w2aJ2ZUp1wEOOFLXgOqvppFngM5OS+qyhl+ZM=
|
||||
github.com/playwright-community/playwright-go v0.5200.0 h1:z/5LGuX2tBrg3ug1HupMXLjIG93f1d2MWdDsNhkMQ9c=
|
||||
github.com/playwright-community/playwright-go v0.5200.0/go.mod h1:UnnyQZaqUOO5ywAZu60+N4EiWReUqX1MQBBA3Oofvf8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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=
|
||||
@@ -85,12 +82,10 @@ 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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
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=
|
||||
@@ -102,8 +97,6 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf
|
||||
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=
|
||||
@@ -120,17 +113,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.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=
|
||||
@@ -139,8 +130,10 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -152,14 +145,16 @@ 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/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
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.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
|
||||
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
|
||||
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=
|
||||
@@ -176,8 +171,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
||||
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
||||
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
|
||||
@@ -134,6 +134,9 @@ func computeXFFHeader(remoteAddr string, origXFFHeader string, pref XFFComputePr
|
||||
origForwardedList := make([]string, 0, 4)
|
||||
if origXFFHeader != "" {
|
||||
origForwardedList = strings.Split(origXFFHeader, ",")
|
||||
for i := range origForwardedList {
|
||||
origForwardedList[i] = strings.TrimSpace(origForwardedList[i])
|
||||
}
|
||||
}
|
||||
origForwardedList = append(origForwardedList, parsedRemoteIP.String())
|
||||
forwardedList := make([]string, 0, len(origForwardedList))
|
||||
|
||||
@@ -2,9 +2,11 @@ package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InitSlog(level string) {
|
||||
@@ -34,3 +36,24 @@ func GetRequestLogger(r *http.Request) *slog.Logger {
|
||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
||||
)
|
||||
}
|
||||
|
||||
// ErrorLogFilter is used to suppress "context canceled" logs from the http server when a request is canceled (e.g., when a client disconnects).
|
||||
type ErrorLogFilter struct {
|
||||
Unwrap *log.Logger
|
||||
}
|
||||
|
||||
func (elf *ErrorLogFilter) Write(p []byte) (n int, err error) {
|
||||
logMessage := string(p)
|
||||
if strings.Contains(logMessage, "context canceled") {
|
||||
return len(p), nil // Suppress the log by doing nothing
|
||||
}
|
||||
if elf.Unwrap != nil {
|
||||
return elf.Unwrap.Writer().Write(p)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func GetFilteredHTTPLogger() *log.Logger {
|
||||
stdErrLogger := log.New(os.Stderr, "", log.LstdFlags) // essentially what the default logger is.
|
||||
return log.New(&ErrorLogFilter{Unwrap: stdErrLogger}, "", 0)
|
||||
}
|
||||
46
internal/log_test.go
Normal file
46
internal/log_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestErrorLogFilter(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
destLogger := log.New(&buf, "", 0)
|
||||
errorFilterWriter := &ErrorLogFilter{Unwrap: destLogger}
|
||||
testErrorLogger := log.New(errorFilterWriter, "", 0)
|
||||
|
||||
// Test Case 1: Suppressed message
|
||||
suppressedMessage := "http: proxy error: context canceled"
|
||||
testErrorLogger.Println(suppressedMessage)
|
||||
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("Suppressed message was written to output. Output: %q", buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
|
||||
// Test Case 2: Allowed message
|
||||
allowedMessage := "http: another error occurred"
|
||||
testErrorLogger.Println(allowedMessage)
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, allowedMessage) {
|
||||
t.Errorf("Allowed message was not written to output. Output: %q", output)
|
||||
}
|
||||
if !strings.HasSuffix(output, "\n") {
|
||||
t.Errorf("Allowed message output is missing newline. Output: %q", output)
|
||||
}
|
||||
buf.Reset()
|
||||
|
||||
// Test Case 3: Partially matching message (should be suppressed)
|
||||
partiallyMatchingMessage := "Some other log before http: proxy error: context canceled and after"
|
||||
testErrorLogger.Println(partiallyMatchingMessage)
|
||||
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("Partially matching message was written to output. Output: %q", buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
@@ -8,18 +8,21 @@ import (
|
||||
)
|
||||
|
||||
// GetOGTags is the main function that retrieves Open Graph tags for a URL
|
||||
func (c *OGTagCache) GetOGTags(url *url.URL) (map[string]string, error) {
|
||||
func (c *OGTagCache) GetOGTags(url *url.URL, originalHost string) (map[string]string, error) {
|
||||
if url == nil {
|
||||
return nil, errors.New("nil URL provided, cannot fetch OG tags")
|
||||
}
|
||||
urlStr := c.getTarget(url)
|
||||
|
||||
target := c.getTarget(url)
|
||||
cacheKey := c.generateCacheKey(target, originalHost)
|
||||
|
||||
// Check cache first
|
||||
if cachedTags := c.checkCache(urlStr); cachedTags != nil {
|
||||
if cachedTags := c.checkCache(cacheKey); cachedTags != nil {
|
||||
return cachedTags, nil
|
||||
}
|
||||
|
||||
// Fetch HTML content
|
||||
doc, err := c.fetchHTMLDocument(urlStr)
|
||||
// Fetch HTML content, passing the original host
|
||||
doc, err := c.fetchHTMLDocumentWithCache(target, originalHost, cacheKey)
|
||||
if errors.Is(err, syscall.ECONNREFUSED) {
|
||||
slog.Debug("Connection refused, returning empty tags")
|
||||
return nil, nil
|
||||
@@ -35,17 +38,28 @@ func (c *OGTagCache) GetOGTags(url *url.URL) (map[string]string, error) {
|
||||
ogTags := c.extractOGTags(doc)
|
||||
|
||||
// Store in cache
|
||||
c.cache.Set(urlStr, ogTags, c.ogTimeToLive)
|
||||
c.cache.Set(cacheKey, ogTags, c.ogTimeToLive)
|
||||
|
||||
return ogTags, nil
|
||||
}
|
||||
|
||||
func (c *OGTagCache) generateCacheKey(target string, originalHost string) string {
|
||||
var cacheKey string
|
||||
|
||||
if c.ogCacheConsiderHost {
|
||||
cacheKey = target + "|" + originalHost
|
||||
} else {
|
||||
cacheKey = target
|
||||
}
|
||||
return cacheKey
|
||||
}
|
||||
|
||||
// checkCache checks if we have the tags cached and returns them if so
|
||||
func (c *OGTagCache) checkCache(urlStr string) map[string]string {
|
||||
if cachedTags, ok := c.cache.Get(urlStr); ok {
|
||||
func (c *OGTagCache) checkCache(cacheKey string) map[string]string {
|
||||
if cachedTags, ok := c.cache.Get(cacheKey); ok {
|
||||
slog.Debug("cache hit", "tags", cachedTags)
|
||||
return cachedTags
|
||||
}
|
||||
slog.Debug("cache miss", "url", urlStr)
|
||||
slog.Debug("cache miss", "url", cacheKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCheckCache(t *testing.T) {
|
||||
cache := NewOGTagCache("http://example.com", true, time.Minute)
|
||||
cache := NewOGTagCache("http://example.com", true, time.Minute, false)
|
||||
|
||||
// Set up test data
|
||||
urlStr := "http://example.com/page"
|
||||
@@ -17,18 +18,19 @@ func TestCheckCache(t *testing.T) {
|
||||
"og:title": "Test Title",
|
||||
"og:description": "Test Description",
|
||||
}
|
||||
cacheKey := cache.generateCacheKey(urlStr, "example.com")
|
||||
|
||||
// Test cache miss
|
||||
tags := cache.checkCache(urlStr)
|
||||
tags := cache.checkCache(cacheKey)
|
||||
if tags != nil {
|
||||
t.Errorf("expected nil tags on cache miss, got %v", tags)
|
||||
}
|
||||
|
||||
// Manually add to cache
|
||||
cache.cache.Set(urlStr, expectedTags, time.Minute)
|
||||
cache.cache.Set(cacheKey, expectedTags, time.Minute)
|
||||
|
||||
// Test cache hit
|
||||
tags = cache.checkCache(urlStr)
|
||||
tags = cache.checkCache(cacheKey)
|
||||
if tags == nil {
|
||||
t.Fatal("expected non-nil tags on cache hit, got nil")
|
||||
}
|
||||
@@ -67,7 +69,7 @@ func TestGetOGTags(t *testing.T) {
|
||||
defer ts.Close()
|
||||
|
||||
// Create an instance of OGTagCache with a short TTL for testing
|
||||
cache := NewOGTagCache(ts.URL, true, 1*time.Minute)
|
||||
cache := NewOGTagCache(ts.URL, true, 1*time.Minute, false)
|
||||
|
||||
// Parse the test server URL
|
||||
parsedURL, err := url.Parse(ts.URL)
|
||||
@@ -76,7 +78,8 @@ func TestGetOGTags(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test fetching OG tags from the test server
|
||||
ogTags, err := cache.GetOGTags(parsedURL)
|
||||
// Pass the host from the parsed test server URL
|
||||
ogTags, err := cache.GetOGTags(parsedURL, parsedURL.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get OG tags: %v", err)
|
||||
}
|
||||
@@ -95,13 +98,15 @@ func TestGetOGTags(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test fetching OG tags from the cache
|
||||
ogTags, err = cache.GetOGTags(parsedURL)
|
||||
// Pass the host from the parsed test server URL
|
||||
ogTags, err = cache.GetOGTags(parsedURL, parsedURL.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get OG tags from cache: %v", err)
|
||||
}
|
||||
|
||||
// Test fetching OG tags from the cache (3rd time)
|
||||
newOgTags, err := cache.GetOGTags(parsedURL)
|
||||
// Pass the host from the parsed test server URL
|
||||
newOgTags, err := cache.GetOGTags(parsedURL, parsedURL.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get OG tags from cache: %v", err)
|
||||
}
|
||||
@@ -120,3 +125,116 @@ func TestGetOGTags(t *testing.T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetOGTagsWithHostConsideration tests the behavior of the cache with and without host consideration and for multiple hosts in a theoretical setup.
|
||||
func TestGetOGTagsWithHostConsideration(t *testing.T) {
|
||||
var loadCount int // Counter to track how many times the test route is loaded
|
||||
|
||||
// Create a test server
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
loadCount++ // Increment counter on each request to the server
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="Test Title" />
|
||||
<meta property="og:description" content="Test Description" />
|
||||
</head>
|
||||
<body><p>Content</p></body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
parsedURL, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse test server URL: %v", err)
|
||||
}
|
||||
|
||||
expectedTags := map[string]string{
|
||||
"og:title": "Test Title",
|
||||
"og:description": "Test Description",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ogCacheConsiderHost bool
|
||||
requests []struct {
|
||||
host string
|
||||
expectedLoadCount int // Expected load count *after* this request
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "Host Not Considered - Same Host",
|
||||
ogCacheConsiderHost: false,
|
||||
requests: []struct {
|
||||
host string
|
||||
expectedLoadCount int
|
||||
}{
|
||||
{"host1", 1}, // First request, miss
|
||||
{"host1", 1}, // Second request, same host, hit (host ignored)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Host Not Considered - Different Host",
|
||||
ogCacheConsiderHost: false,
|
||||
requests: []struct {
|
||||
host string
|
||||
expectedLoadCount int
|
||||
}{
|
||||
{"host1", 1}, // First request, miss
|
||||
{"host2", 1}, // Second request, different host, hit (host ignored)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Host Considered - Same Host",
|
||||
ogCacheConsiderHost: true,
|
||||
requests: []struct {
|
||||
host string
|
||||
expectedLoadCount int
|
||||
}{
|
||||
{"host1", 1}, // First request, miss
|
||||
{"host1", 1}, // Second request, same host, hit
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Host Considered - Different Host",
|
||||
ogCacheConsiderHost: true,
|
||||
requests: []struct {
|
||||
host string
|
||||
expectedLoadCount int
|
||||
}{
|
||||
{"host1", 1}, // First request, miss
|
||||
{"host2", 2}, // Second request, different host, miss
|
||||
{"host2", 2}, // Third request, same as second, hit
|
||||
{"host1", 2}, // Fourth request, same as first, hit
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
loadCount = 0 // Reset load count for each test case
|
||||
cache := NewOGTagCache(ts.URL, true, 1*time.Minute, tc.ogCacheConsiderHost)
|
||||
|
||||
for i, req := range tc.requests {
|
||||
ogTags, err := cache.GetOGTags(parsedURL, req.host)
|
||||
if err != nil {
|
||||
t.Errorf("Request %d (host: %s): unexpected error: %v", i+1, req.host, err)
|
||||
continue // Skip further checks for this request if error occurred
|
||||
}
|
||||
|
||||
// Verify tags are correct (should always be the same in this setup)
|
||||
if !reflect.DeepEqual(ogTags, expectedTags) {
|
||||
t.Errorf("Request %d (host: %s): expected tags %v, got %v", i+1, req.host, expectedTags, ogTags)
|
||||
}
|
||||
|
||||
// Verify the load count to check cache hit/miss behavior
|
||||
if loadCount != req.expectedLoadCount {
|
||||
t.Errorf("Request %d (host: %s): expected load count %d, got %d (cache hit/miss mismatch)", i+1, req.host, req.expectedLoadCount, loadCount)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ogtags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/net/html"
|
||||
@@ -16,17 +17,35 @@ var (
|
||||
emptyMap = map[string]string{} // used to indicate an empty result in the cache. Can't use nil as it would be a cache miss.
|
||||
)
|
||||
|
||||
func (c *OGTagCache) fetchHTMLDocument(urlStr string) (*html.Node, error) {
|
||||
resp, err := c.client.Get(urlStr)
|
||||
// fetchHTMLDocumentWithCache fetches the HTML document from the given URL string,
|
||||
// preserving the original host header.
|
||||
func (c *OGTagCache) fetchHTMLDocumentWithCache(urlStr string, originalHost string, cacheKey string) (*html.Node, error) {
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create http request: %w", err)
|
||||
}
|
||||
|
||||
// Set the Host header to the original host
|
||||
if originalHost != "" {
|
||||
req.Host = originalHost
|
||||
}
|
||||
|
||||
// Add proxy headers
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
req.Header.Set("User-Agent", "Anubis-OGTag-Fetcher/1.0") // For tracking purposes
|
||||
|
||||
// Send the request
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
slog.Debug("og: request timed out", "url", urlStr)
|
||||
c.cache.Set(urlStr, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server
|
||||
c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server
|
||||
}
|
||||
return nil, fmt.Errorf("http get failed: %w", err)
|
||||
}
|
||||
// this defer will call MaxBytesReader's Close, which closes the original body.
|
||||
|
||||
// Ensure the response body is closed
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
@@ -36,19 +55,17 @@ func (c *OGTagCache) fetchHTMLDocument(urlStr string) (*html.Node, error) {
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
slog.Debug("og: received non-OK status code", "url", urlStr, "status", resp.StatusCode)
|
||||
c.cache.Set(urlStr, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes
|
||||
c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes
|
||||
return nil, fmt.Errorf("%w: page not found", ErrOgHandled)
|
||||
}
|
||||
|
||||
// Check content type
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
if ct == "" {
|
||||
// assume non html body
|
||||
return nil, fmt.Errorf("missing Content-Type header")
|
||||
} else {
|
||||
mediaType, _, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
// Malformed Content-Type header
|
||||
slog.Debug("og: malformed Content-Type header", "url", urlStr, "contentType", ct)
|
||||
return nil, fmt.Errorf("%w malformed Content-Type header: %w", ErrOgHandled, err)
|
||||
}
|
||||
@@ -59,17 +76,16 @@ func (c *OGTagCache) fetchHTMLDocument(urlStr string) (*html.Node, error) {
|
||||
}
|
||||
}
|
||||
|
||||
resp.Body = http.MaxBytesReader(nil, resp.Body, c.maxContentLength)
|
||||
resp.Body = http.MaxBytesReader(nil, resp.Body, maxContentLength)
|
||||
|
||||
doc, err := html.Parse(resp.Body)
|
||||
if err != nil {
|
||||
// Check if the error is specifically because the limit was exceeded
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
if errors.As(err, &maxBytesErr) {
|
||||
slog.Debug("og: content exceeded max length", "url", urlStr, "limit", c.maxContentLength)
|
||||
return nil, fmt.Errorf("content too large: exceeded %d bytes", c.maxContentLength)
|
||||
slog.Debug("og: content exceeded max length", "url", urlStr, "limit", maxContentLength)
|
||||
return nil, fmt.Errorf("content too large: exceeded %d bytes", maxContentLength)
|
||||
}
|
||||
// parsing error (e.g., malformed HTML)
|
||||
return nil, fmt.Errorf("failed to parse HTML: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package ogtags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/net/html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -78,8 +79,8 @@ func TestFetchHTMLDocument(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cache := NewOGTagCache("", true, time.Minute)
|
||||
doc, err := cache.fetchHTMLDocument(ts.URL)
|
||||
cache := NewOGTagCache("", true, time.Minute, false)
|
||||
doc, err := cache.fetchHTMLDocument(ts.URL, "anything")
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
@@ -105,9 +106,9 @@ func TestFetchHTMLDocumentInvalidURL(t *testing.T) {
|
||||
t.Skip("test requires theoretical network egress")
|
||||
}
|
||||
|
||||
cache := NewOGTagCache("", true, time.Minute)
|
||||
cache := NewOGTagCache("", true, time.Minute, false)
|
||||
|
||||
doc, err := cache.fetchHTMLDocument("http://invalid.url.that.doesnt.exist.example")
|
||||
doc, err := cache.fetchHTMLDocument("http://invalid.url.that.doesnt.exist.example", "anything")
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid URL, got nil")
|
||||
@@ -117,3 +118,9 @@ func TestFetchHTMLDocumentInvalidURL(t *testing.T) {
|
||||
t.Error("expected nil document for invalid URL, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
// fetchHTMLDocument allows you to call fetchHTMLDocumentWithCache without a duplicate generateCacheKey call
|
||||
func (c *OGTagCache) fetchHTMLDocument(urlStr string, originalHost string) (*html.Node, error) {
|
||||
cacheKey := c.generateCacheKey(urlStr, originalHost)
|
||||
return c.fetchHTMLDocumentWithCache(urlStr, originalHost, cacheKey)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Non-existent page",
|
||||
name: "Nonexistent page",
|
||||
path: "/not-found",
|
||||
query: "",
|
||||
expectedTags: nil,
|
||||
@@ -104,7 +104,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create cache instance
|
||||
cache := NewOGTagCache(ts.URL, true, 1*time.Minute)
|
||||
cache := NewOGTagCache(ts.URL, true, 1*time.Minute, false)
|
||||
|
||||
// Create URL for test
|
||||
testURL, _ := url.Parse(ts.URL)
|
||||
@@ -112,7 +112,8 @@ func TestIntegrationGetOGTags(t *testing.T) {
|
||||
testURL.RawQuery = tc.query
|
||||
|
||||
// Get OG tags
|
||||
ogTags, err := cache.GetOGTags(testURL)
|
||||
// Pass the host from the test URL
|
||||
ogTags, err := cache.GetOGTags(testURL, testURL.Host)
|
||||
|
||||
// Check error expectation
|
||||
if tc.expectError {
|
||||
@@ -139,7 +140,8 @@ func TestIntegrationGetOGTags(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test cache retrieval
|
||||
cachedOGTags, err := cache.GetOGTags(testURL)
|
||||
// Pass the host from the test URL
|
||||
cachedOGTags, err := cache.GetOGTags(testURL, testURL.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get OG tags from cache: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,51 +1,111 @@
|
||||
package ogtags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/decaymap"
|
||||
)
|
||||
|
||||
const (
|
||||
maxContentLength = 16 << 20 // 16 MiB in bytes, if there is a reasonable reason that you need more than this...Why?
|
||||
httpTimeout = 5 * time.Second /*todo: make this configurable?*/
|
||||
)
|
||||
|
||||
type OGTagCache struct {
|
||||
cache *decaymap.Impl[string, map[string]string]
|
||||
target string
|
||||
ogPassthrough bool
|
||||
ogTimeToLive time.Duration
|
||||
approvedTags []string
|
||||
approvedPrefixes []string
|
||||
client *http.Client
|
||||
maxContentLength int64
|
||||
cache *decaymap.Impl[string, map[string]string]
|
||||
targetURL *url.URL
|
||||
ogCacheConsiderHost bool
|
||||
ogPassthrough bool
|
||||
ogTimeToLive time.Duration
|
||||
approvedTags []string
|
||||
approvedPrefixes []string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewOGTagCache(target string, ogPassthrough bool, ogTimeToLive time.Duration) *OGTagCache {
|
||||
func NewOGTagCache(target string, ogPassthrough bool, ogTimeToLive time.Duration, ogTagsConsiderHost bool) *OGTagCache {
|
||||
// Predefined approved tags and prefixes
|
||||
// In the future, these could come from configuration
|
||||
defaultApprovedTags := []string{"description", "keywords", "author"}
|
||||
defaultApprovedPrefixes := []string{"og:", "twitter:", "fediverse:"}
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second, /*make this configurable?*/
|
||||
|
||||
var parsedTargetURL *url.URL
|
||||
var err error
|
||||
|
||||
if target == "" {
|
||||
// Default to localhost if target is empty
|
||||
parsedTargetURL, _ = url.Parse("http://localhost")
|
||||
} else {
|
||||
parsedTargetURL, err = url.Parse(target)
|
||||
if err != nil {
|
||||
slog.Debug("og: failed to parse target URL, treating as non-unix", "target", target, "error", err)
|
||||
// If parsing fails, treat it as a non-unix target for backward compatibility or default behavior
|
||||
// For now, assume it's not a scheme issue but maybe an invalid char, etc.
|
||||
// A simple string target might be intended if it's not a full URL.
|
||||
parsedTargetURL = &url.URL{Scheme: "http", Host: target} // Assume http if scheme missing and host-like
|
||||
if !strings.Contains(target, "://") && !strings.HasPrefix(target, "unix:") {
|
||||
// If it looks like just a host/host:port (and not unix), prepend http:// (todo: is this bad...? Trace path to see if i can yell at user to do it right)
|
||||
parsedTargetURL, _ = url.Parse("http://" + target) // fetch cares about scheme but anubis doesn't
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxContentLength = 16 << 20 // 16 MiB in bytes
|
||||
client := &http.Client{
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
|
||||
// Configure custom transport for Unix sockets
|
||||
if parsedTargetURL.Scheme == "unix" {
|
||||
socketPath := parsedTargetURL.Path // For unix scheme, path is the socket path
|
||||
client.Transport = &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", socketPath)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &OGTagCache{
|
||||
cache: decaymap.New[string, map[string]string](),
|
||||
target: target,
|
||||
ogPassthrough: ogPassthrough,
|
||||
ogTimeToLive: ogTimeToLive,
|
||||
approvedTags: defaultApprovedTags,
|
||||
approvedPrefixes: defaultApprovedPrefixes,
|
||||
client: client,
|
||||
maxContentLength: maxContentLength,
|
||||
cache: decaymap.New[string, map[string]string](),
|
||||
targetURL: parsedTargetURL, // Store the parsed URL
|
||||
ogPassthrough: ogPassthrough,
|
||||
ogTimeToLive: ogTimeToLive,
|
||||
ogCacheConsiderHost: ogTagsConsiderHost, // todo: refactor to be a separate struct
|
||||
approvedTags: defaultApprovedTags,
|
||||
approvedPrefixes: defaultApprovedPrefixes,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// getTarget constructs the target URL string for fetching OG tags.
|
||||
// For Unix sockets, it creates a "fake" HTTP URL that the custom dialer understands.
|
||||
func (c *OGTagCache) getTarget(u *url.URL) string {
|
||||
return c.target + u.Path
|
||||
if c.targetURL.Scheme == "unix" {
|
||||
// The custom dialer ignores the host, but we need a valid http URL structure.
|
||||
// Use "unix" as a placeholder host. Path and Query from original request are appended.
|
||||
fakeURL := &url.URL{
|
||||
Scheme: "http", // Scheme must be http/https for client.Get
|
||||
Host: "unix", // Arbitrary host, ignored by custom dialer
|
||||
Path: u.Path,
|
||||
RawQuery: u.RawQuery,
|
||||
}
|
||||
return fakeURL.String()
|
||||
}
|
||||
|
||||
// For regular http/https targets
|
||||
target := *c.targetURL // Make a copy
|
||||
target.Path = u.Path
|
||||
target.RawQuery = u.RawQuery
|
||||
return target.String()
|
||||
|
||||
}
|
||||
|
||||
func (c *OGTagCache) Cleanup() {
|
||||
c.cache.Cleanup()
|
||||
if c.cache != nil {
|
||||
c.cache.Cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
package ogtags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -29,14 +38,23 @@ func TestNewOGTagCache(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cache := NewOGTagCache(tt.target, tt.ogPassthrough, tt.ogTimeToLive)
|
||||
cache := NewOGTagCache(tt.target, tt.ogPassthrough, tt.ogTimeToLive, false)
|
||||
|
||||
if cache == nil {
|
||||
t.Fatal("expected non-nil cache, got nil")
|
||||
}
|
||||
|
||||
if cache.target != tt.target {
|
||||
t.Errorf("expected target %s, got %s", tt.target, cache.target)
|
||||
// Check the parsed targetURL, handling the default case for empty target
|
||||
expectedURLStr := tt.target
|
||||
if tt.target == "" {
|
||||
// Default behavior when target is empty is now http://localhost
|
||||
expectedURLStr = "http://localhost"
|
||||
} else if !strings.Contains(tt.target, "://") && !strings.HasPrefix(tt.target, "unix:") {
|
||||
// Handle case where target is just host or host:port (and not unix)
|
||||
expectedURLStr = "http://" + tt.target
|
||||
}
|
||||
if cache.targetURL.String() != expectedURLStr {
|
||||
t.Errorf("expected targetURL %s, got %s", expectedURLStr, cache.targetURL.String())
|
||||
}
|
||||
|
||||
if cache.ogPassthrough != tt.ogPassthrough {
|
||||
@@ -50,6 +68,45 @@ func TestNewOGTagCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewOGTagCache_UnixSocket specifically tests unix socket initialization
|
||||
func TestNewOGTagCache_UnixSocket(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
socketPath := filepath.Join(tempDir, "test.sock")
|
||||
target := "unix://" + socketPath
|
||||
|
||||
cache := NewOGTagCache(target, true, 5*time.Minute, false)
|
||||
|
||||
if cache == nil {
|
||||
t.Fatal("expected non-nil cache, got nil")
|
||||
}
|
||||
|
||||
if cache.targetURL.Scheme != "unix" {
|
||||
t.Errorf("expected targetURL scheme 'unix', got '%s'", cache.targetURL.Scheme)
|
||||
}
|
||||
if cache.targetURL.Path != socketPath {
|
||||
t.Errorf("expected targetURL path '%s', got '%s'", socketPath, cache.targetURL.Path)
|
||||
}
|
||||
|
||||
// Check if the client transport is configured for Unix sockets
|
||||
transport, ok := cache.client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
t.Fatalf("expected client transport to be *http.Transport, got %T", cache.client.Transport)
|
||||
}
|
||||
if transport.DialContext == nil {
|
||||
t.Fatal("expected client transport DialContext to be non-nil for unix socket")
|
||||
}
|
||||
|
||||
// Attempt a dummy dial to see if it uses the correct path (optional, more involved check)
|
||||
dummyConn, err := transport.DialContext(context.Background(), "", "")
|
||||
if err == nil {
|
||||
dummyConn.Close()
|
||||
t.Log("DialContext seems functional, but couldn't verify path without a listener")
|
||||
} else if !strings.Contains(err.Error(), "connect: connection refused") && !strings.Contains(err.Error(), "connect: no such file or directory") {
|
||||
// We expect connection refused or not found if nothing is listening
|
||||
t.Errorf("DialContext failed with unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTarget(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -66,24 +123,39 @@ func TestGetTarget(t *testing.T) {
|
||||
expected: "http://example.com",
|
||||
},
|
||||
{
|
||||
name: "With complex path",
|
||||
target: "http://example.com",
|
||||
path: "/pag(#*((#@)ΓΓΓΓe/Γ",
|
||||
query: "id=123",
|
||||
expected: "http://example.com/pag(#*((#@)ΓΓΓΓe/Γ",
|
||||
name: "With complex path",
|
||||
target: "http://example.com",
|
||||
path: "/pag(#*((#@)ΓΓΓΓe/Γ",
|
||||
query: "id=123",
|
||||
// Expect URL encoding and query parameter
|
||||
expected: "http://example.com/pag%28%23%2A%28%28%23@%29%CE%93%CE%93%CE%93%CE%93e/%CE%93?id=123",
|
||||
},
|
||||
{
|
||||
name: "With query and path",
|
||||
target: "http://example.com",
|
||||
path: "/page",
|
||||
query: "id=123",
|
||||
expected: "http://example.com/page",
|
||||
expected: "http://example.com/page?id=123",
|
||||
},
|
||||
{
|
||||
name: "Unix socket target",
|
||||
target: "unix:/tmp/anubis.sock",
|
||||
path: "/some/path",
|
||||
query: "key=value&flag=true",
|
||||
expected: "http://unix/some/path?key=value&flag=true", // Scheme becomes http, host is 'unix'
|
||||
},
|
||||
{
|
||||
name: "Unix socket target with ///",
|
||||
target: "unix:///var/run/anubis.sock",
|
||||
path: "/",
|
||||
query: "",
|
||||
expected: "http://unix/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cache := NewOGTagCache(tt.target, false, time.Minute)
|
||||
cache := NewOGTagCache(tt.target, false, time.Minute, false)
|
||||
|
||||
u := &url.URL{
|
||||
Path: tt.path,
|
||||
@@ -98,3 +170,86 @@ func TestGetTarget(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationGetOGTags_UnixSocket tests fetching OG tags via a Unix socket.
|
||||
func TestIntegrationGetOGTags_UnixSocket(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
socketPath := filepath.Join(tempDir, "anubis-test.sock")
|
||||
|
||||
// Ensure the socket does not exist initially
|
||||
_ = os.Remove(socketPath)
|
||||
|
||||
// Create a simple HTTP server listening on the Unix socket
|
||||
listener, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen on unix socket %s: %v", socketPath, err)
|
||||
}
|
||||
defer func(listener net.Listener, socketPath string) {
|
||||
if listener != nil {
|
||||
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
t.Logf("Error closing listener: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(socketPath); err == nil {
|
||||
if err := os.Remove(socketPath); err != nil {
|
||||
t.Logf("Error removing socket file %s: %v", socketPath, err)
|
||||
}
|
||||
}
|
||||
}(listener, socketPath)
|
||||
|
||||
server := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintln(w, `<!DOCTYPE html><html><head><meta property="og:title" content="Unix Socket Test" /></head><body>Test</body></html>`)
|
||||
}),
|
||||
}
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
t.Logf("Unix socket server error: %v", err)
|
||||
}
|
||||
}()
|
||||
defer func(server *http.Server, ctx context.Context) {
|
||||
err := server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
t.Logf("Error shutting down server: %v", err)
|
||||
}
|
||||
}(server, context.Background()) // Ensure server is shut down
|
||||
|
||||
// Wait a moment for the server to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Create cache instance pointing to the Unix socket
|
||||
targetURL := "unix://" + socketPath
|
||||
cache := NewOGTagCache(targetURL, true, 1*time.Minute, false)
|
||||
|
||||
// Create a dummy URL for the request (path and query matter)
|
||||
testReqURL, _ := url.Parse("/some/page?query=1")
|
||||
|
||||
// Get OG tags
|
||||
// Pass an empty string for host, as it's irrelevant for unix sockets
|
||||
ogTags, err := cache.GetOGTags(testReqURL, "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetOGTags failed for unix socket: %v", err)
|
||||
}
|
||||
|
||||
expectedTags := map[string]string{
|
||||
"og:title": "Unix Socket Test",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ogTags, expectedTags) {
|
||||
t.Errorf("Expected OG tags %v, got %v", expectedTags, ogTags)
|
||||
}
|
||||
|
||||
// Test cache retrieval (should hit cache)
|
||||
// Pass an empty string for host
|
||||
cachedTags, err := cache.GetOGTags(testReqURL, "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetOGTags (cache hit) failed for unix socket: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(cachedTags, expectedTags) {
|
||||
t.Errorf("Expected cached OG tags %v, got %v", expectedTags, cachedTags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// TestExtractOGTags updated with correct expectations based on filtering logic
|
||||
func TestExtractOGTags(t *testing.T) {
|
||||
// Use a cache instance that reflects the default approved lists
|
||||
testCache := NewOGTagCache("", false, time.Minute)
|
||||
testCache := NewOGTagCache("", false, time.Minute, false)
|
||||
// Manually set approved tags/prefixes based on the user request for clarity
|
||||
testCache.approvedTags = []string{"description"}
|
||||
testCache.approvedPrefixes = []string{"og:"}
|
||||
@@ -189,7 +189,7 @@ func TestIsOGMetaTag(t *testing.T) {
|
||||
|
||||
func TestExtractMetaTagInfo(t *testing.T) {
|
||||
// Use a cache instance that reflects the default approved lists
|
||||
testCache := NewOGTagCache("", false, time.Minute)
|
||||
testCache := NewOGTagCache("", false, time.Minute, false)
|
||||
testCache.approvedTags = []string{"description"}
|
||||
testCache.approvedPrefixes = []string{"og:"}
|
||||
|
||||
|
||||
@@ -130,6 +130,19 @@ func TestComputeXFFHeader(t *testing.T) {
|
||||
},
|
||||
result: "1.1.1.1",
|
||||
},
|
||||
{
|
||||
name: "TrimSpaces",
|
||||
remoteAddr: "127.0.0.1:80",
|
||||
origXFFHeader: "1.1.1.1, 10.0.0.1, fe80::, 100.64.0.1, 169.254.0.1",
|
||||
pref: XFFComputePreferences{
|
||||
StripPrivate: true,
|
||||
StripLoopback: true,
|
||||
StripCGNAT: true,
|
||||
StripLLU: true,
|
||||
Flatten: true,
|
||||
},
|
||||
result: "1.1.1.1",
|
||||
},
|
||||
{
|
||||
name: "invalid-ip-port",
|
||||
remoteAddr: "fe80::",
|
||||
|
||||
@@ -170,7 +170,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
|
||||
hash := rule.Hash()
|
||||
|
||||
lg.Debug("rule hash", "hash", hash)
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), http.StatusOK)
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), s.policy.StatusCodes.Deny)
|
||||
return true
|
||||
case config.RuleChallenge:
|
||||
lg.Debug("challenge requested")
|
||||
@@ -202,7 +202,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
||||
|
||||
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)
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), s.policy.StatusCodes.Deny)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -348,7 +348,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
"response": response,
|
||||
"iat": time.Now().Unix(),
|
||||
"nbf": time.Now().Add(-1 * time.Minute).Unix(),
|
||||
"exp": time.Now().Add(24 * 7 * time.Hour).Unix(),
|
||||
"exp": time.Now().Add(s.opts.CookieExpiration).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString(s.priv)
|
||||
if err != nil {
|
||||
@@ -361,7 +361,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: anubis.CookieName,
|
||||
Value: tokenString,
|
||||
Expires: time.Now().Add(24 * 7 * time.Hour),
|
||||
Expires: time.Now().Add(s.opts.CookieExpiration),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Domain: s.opts.CookieDomain,
|
||||
Partitioned: s.opts.CookiePartitioned,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
@@ -127,17 +128,18 @@ func TestCVE2025_24369(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieSettings(t *testing.T) {
|
||||
func TestCookieCustomExpiration(t *testing.T) {
|
||||
pol := loadPolicies(t, "")
|
||||
pol.DefaultDifficulty = 0
|
||||
ckieExpiration := 10 * time.Minute
|
||||
|
||||
srv := spawnAnubis(t, Options{
|
||||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: "local.cetacean.club",
|
||||
CookiePartitioned: true,
|
||||
CookieName: t.Name(),
|
||||
CookieDomain: "local.cetacean.club",
|
||||
CookieName: t.Name(),
|
||||
CookieExpiration: ckieExpiration,
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
@@ -181,7 +183,99 @@ func TestCookieSettings(t *testing.T) {
|
||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
requestReceiveLowerBound := time.Now()
|
||||
resp, err = cli.Do(req)
|
||||
requestReceiveUpperBound := time.Now()
|
||||
if err != nil {
|
||||
t.Fatalf("can't do challenge passing")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
resp.Write(os.Stderr)
|
||||
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
|
||||
}
|
||||
|
||||
var ckie *http.Cookie
|
||||
for _, cookie := range resp.Cookies() {
|
||||
t.Logf("%#v", cookie)
|
||||
if cookie.Name == anubis.CookieName {
|
||||
ckie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
if ckie == nil {
|
||||
t.Errorf("Cookie %q not found", anubis.CookieName)
|
||||
return
|
||||
}
|
||||
|
||||
expirationLowerBound := requestReceiveLowerBound.Add(ckieExpiration)
|
||||
expirationUpperBound := requestReceiveUpperBound.Add(ckieExpiration)
|
||||
// Since the cookie expiration precision is only to the second due to the Unix() call, we can
|
||||
// lower the level of expected precision.
|
||||
if ckie.Expires.Unix() < expirationLowerBound.Unix() || ckie.Expires.Unix() > expirationUpperBound.Unix() {
|
||||
t.Errorf("cookie expiration is not within the expected range. expected between: %v and %v. got: %v", expirationLowerBound, expirationUpperBound, ckie.Expires)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieSettings(t *testing.T) {
|
||||
pol := loadPolicies(t, "")
|
||||
pol.DefaultDifficulty = 0
|
||||
|
||||
srv := spawnAnubis(t, Options{
|
||||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: "local.cetacean.club",
|
||||
CookiePartitioned: true,
|
||||
CookieName: t.Name(),
|
||||
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
defer ts.Close()
|
||||
|
||||
cli := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't request challenge: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var chall = struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||
t.Fatalf("can't read challenge response body: %v", err)
|
||||
}
|
||||
|
||||
nonce := 0
|
||||
elapsedTime := 420
|
||||
redir := "/"
|
||||
calculated := ""
|
||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
||||
calculated = internal.SHA256sum(calcString)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("response", calculated)
|
||||
q.Set("nonce", fmt.Sprint(nonce))
|
||||
q.Set("redir", redir)
|
||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
requestReceiveLowerBound := time.Now()
|
||||
resp, err = cli.Do(req)
|
||||
requestReceiveUpperBound := time.Now()
|
||||
if err != nil {
|
||||
t.Fatalf("can't do challenge passing")
|
||||
}
|
||||
@@ -208,6 +302,15 @@ func TestCookieSettings(t *testing.T) {
|
||||
t.Errorf("cookie domain is wrong, wanted local.cetacean.club, got: %s", ckie.Domain)
|
||||
}
|
||||
|
||||
expirationLowerBound := requestReceiveLowerBound.Add(anubis.CookieDefaultExpirationTime)
|
||||
expirationUpperBound := requestReceiveUpperBound.Add(anubis.CookieDefaultExpirationTime)
|
||||
// Since the cookie expiration precision is only to the second due to the Unix() call, we can
|
||||
// lower the level of expected precision.
|
||||
if ckie.Expires.Unix() < expirationLowerBound.Unix() || ckie.Expires.Unix() > expirationUpperBound.Unix() {
|
||||
t.Errorf("cookie expiration is not within the expected range. expected between: %v and %v. got: %v", expirationLowerBound, expirationUpperBound, ckie.Expires)
|
||||
return
|
||||
}
|
||||
|
||||
if ckie.Partitioned != srv.opts.CookiePartitioned {
|
||||
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
|
||||
}
|
||||
@@ -395,6 +498,51 @@ func TestBasePrefix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomStatusCodes(t *testing.T) {
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Log(r.UserAgent())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, "OK")
|
||||
})
|
||||
|
||||
statusMap := map[string]int{
|
||||
"ALLOW": 200,
|
||||
"CHALLENGE": 401,
|
||||
"DENY": 403,
|
||||
}
|
||||
|
||||
pol := loadPolicies(t, "./testdata/aggressive_403.yaml")
|
||||
pol.DefaultDifficulty = 4
|
||||
|
||||
srv := spawnAnubis(t, Options{
|
||||
Next: h,
|
||||
Policy: pol,
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
defer ts.Close()
|
||||
|
||||
for userAgent, statusCode := range statusMap {
|
||||
t.Run(userAgent, func(t *testing.T) {
|
||||
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, ts.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != statusCode {
|
||||
t.Errorf("wanted status code %d but got: %d", statusCode, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudflareWorkersRule(t *testing.T) {
|
||||
for _, variant := range []string{"cel", "header"} {
|
||||
t.Run(variant, func(t *testing.T) {
|
||||
|
||||
@@ -29,13 +29,15 @@ type Options struct {
|
||||
ServeRobotsTXT bool
|
||||
PrivateKey ed25519.PrivateKey
|
||||
|
||||
CookieExpiration time.Duration
|
||||
CookieDomain string
|
||||
CookieName string
|
||||
CookiePartitioned bool
|
||||
|
||||
OGPassthrough bool
|
||||
OGTimeToLive time.Duration
|
||||
Target string
|
||||
OGPassthrough bool
|
||||
OGTimeToLive time.Duration
|
||||
OGCacheConsidersHost bool
|
||||
Target string
|
||||
|
||||
WebmasterEmail string
|
||||
BasePrefix string
|
||||
@@ -89,7 +91,7 @@ func New(opts Options) (*Server, error) {
|
||||
policy: opts.Policy,
|
||||
opts: opts,
|
||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive),
|
||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive, opts.OGCacheConsidersHost),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -54,7 +54,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
var ogTags map[string]string = nil
|
||||
if s.opts.OGPassthrough {
|
||||
var err error
|
||||
ogTags, err = s.OGTags.GetOGTags(r.URL)
|
||||
ogTags, err = s.OGTags.GetOGTags(r.URL, r.Host)
|
||||
if err != nil {
|
||||
lg.Error("failed to get OG tags", "err", err)
|
||||
}
|
||||
@@ -67,7 +67,10 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
return
|
||||
}
|
||||
|
||||
handler := internal.NoStoreCache(templ.Handler(component))
|
||||
handler := internal.NoStoreCache(templ.Handler(
|
||||
component,
|
||||
templ.WithStatus(s.opts.Policy.StatusCodes.Challenge),
|
||||
))
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ func NewCELChecker(cfg *config.ExpressionOrList) (*CELChecker, error) {
|
||||
if cfg.Expression != "" {
|
||||
src = cfg.Expression
|
||||
var iss *cel.Issues
|
||||
interm, iss := env.Compile(src)
|
||||
intermediate, iss := env.Compile(src)
|
||||
if iss != nil {
|
||||
return nil, iss.Err()
|
||||
}
|
||||
|
||||
ast, iss = env.Check(interm)
|
||||
ast, iss = env.Check(intermediate)
|
||||
if iss != nil {
|
||||
return nil, iss.Err()
|
||||
}
|
||||
@@ -102,12 +102,6 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -28,6 +29,7 @@ var (
|
||||
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")
|
||||
ErrStatusCodeNotValid = errors.New("config.StatusCode: status code not valid, must be between 100 and 599")
|
||||
)
|
||||
|
||||
type Rule string
|
||||
@@ -227,18 +229,27 @@ func (is *ImportStatement) load() error {
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
var imported []BotOrImport
|
||||
var result []BotConfig
|
||||
|
||||
if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&result); err != nil {
|
||||
if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&imported); err != nil {
|
||||
return fmt.Errorf("can't parse %s: %w", is.Import, err)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
for _, b := range result {
|
||||
for _, b := range imported {
|
||||
if err := b.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if b.ImportStatement != nil {
|
||||
result = append(result, b.ImportStatement.Bots...)
|
||||
}
|
||||
|
||||
if b.BotConfig != nil {
|
||||
result = append(result, *b.BotConfig)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
@@ -275,9 +286,33 @@ func (boi *BotOrImport) Valid() error {
|
||||
return ErrMustSetBotOrImportRules
|
||||
}
|
||||
|
||||
type StatusCodes struct {
|
||||
Challenge int `json:"CHALLENGE"`
|
||||
Deny int `json:"DENY"`
|
||||
}
|
||||
|
||||
func (sc StatusCodes) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if sc.Challenge == 0 || (sc.Challenge < 100 && sc.Challenge >= 599) {
|
||||
errs = append(errs, fmt.Errorf("%w: challenge is %d", ErrStatusCodeNotValid, sc.Challenge))
|
||||
}
|
||||
|
||||
if sc.Deny == 0 || (sc.Deny < 100 && sc.Deny >= 599) {
|
||||
errs = append(errs, fmt.Errorf("%w: deny is %d", ErrStatusCodeNotValid, sc.Deny))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("status codes not valid:\n%w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileConfig struct {
|
||||
Bots []BotOrImport `json:"bots"`
|
||||
DNSBL bool `json:"dnsbl"`
|
||||
Bots []BotOrImport `json:"bots"`
|
||||
DNSBL bool `json:"dnsbl"`
|
||||
StatusCodes StatusCodes `json:"status_codes"`
|
||||
}
|
||||
|
||||
func (c fileConfig) Valid() error {
|
||||
@@ -293,6 +328,10 @@ func (c fileConfig) Valid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.StatusCodes.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
|
||||
}
|
||||
@@ -302,6 +341,10 @@ func (c fileConfig) Valid() error {
|
||||
|
||||
func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
var c fileConfig
|
||||
c.StatusCodes = StatusCodes{
|
||||
Challenge: http.StatusOK,
|
||||
Deny: http.StatusOK,
|
||||
}
|
||||
if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&c); err != nil {
|
||||
return nil, fmt.Errorf("can't parse policy config YAML %s: %w", fname, err)
|
||||
}
|
||||
@@ -311,7 +354,8 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
}
|
||||
|
||||
result := &Config{
|
||||
DNSBL: c.DNSBL,
|
||||
DNSBL: c.DNSBL,
|
||||
StatusCodes: c.StatusCodes,
|
||||
}
|
||||
|
||||
var validationErrs []error
|
||||
@@ -344,8 +388,9 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Bots []BotConfig
|
||||
DNSBL bool
|
||||
Bots []BotConfig
|
||||
DNSBL bool
|
||||
StatusCodes StatusCodes
|
||||
}
|
||||
|
||||
func (c Config) Valid() error {
|
||||
|
||||
13
lib/policy/config/testdata/bad/status-codes-0.json
vendored
Normal file
13
lib/policy/config/testdata/bad/status-codes-0.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "everything",
|
||||
"user_agent_regex": ".*",
|
||||
"action": "DENY"
|
||||
}
|
||||
],
|
||||
"status_codes": {
|
||||
"CHALLENGE": 0,
|
||||
"DENY": 0
|
||||
}
|
||||
}
|
||||
8
lib/policy/config/testdata/bad/status-codes-0.yaml
vendored
Normal file
8
lib/policy/config/testdata/bad/status-codes-0.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
bots:
|
||||
- name: everything
|
||||
user_agent_regex: .*
|
||||
action: DENY
|
||||
|
||||
status_codes:
|
||||
CHALLENGE: 0
|
||||
DENY: 0
|
||||
13
lib/policy/config/testdata/good/status-codes-paranoid.json
vendored
Normal file
13
lib/policy/config/testdata/good/status-codes-paranoid.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "everything",
|
||||
"user_agent_regex": ".*",
|
||||
"action": "DENY"
|
||||
}
|
||||
],
|
||||
"status_codes": {
|
||||
"CHALLENGE": 200,
|
||||
"DENY": 200
|
||||
}
|
||||
}
|
||||
8
lib/policy/config/testdata/good/status-codes-paranoid.yaml
vendored
Normal file
8
lib/policy/config/testdata/good/status-codes-paranoid.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
bots:
|
||||
- name: everything
|
||||
user_agent_regex: .*
|
||||
action: DENY
|
||||
|
||||
status_codes:
|
||||
CHALLENGE: 200
|
||||
DENY: 200
|
||||
13
lib/policy/config/testdata/good/status-codes-rfc.json
vendored
Normal file
13
lib/policy/config/testdata/good/status-codes-rfc.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "everything",
|
||||
"user_agent_regex": ".*",
|
||||
"action": "DENY"
|
||||
}
|
||||
],
|
||||
"status_codes": {
|
||||
"CHALLENGE": 403,
|
||||
"DENY": 403
|
||||
}
|
||||
}
|
||||
8
lib/policy/config/testdata/good/status-codes-rfc.yaml
vendored
Normal file
8
lib/policy/config/testdata/good/status-codes-rfc.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
bots:
|
||||
- name: everything
|
||||
user_agent_regex: .*
|
||||
action: DENY
|
||||
|
||||
status_codes:
|
||||
CHALLENGE: 403
|
||||
DENY: 403
|
||||
@@ -1,11 +1,7 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -24,8 +20,6 @@ func NewEnvironment() (*cel.Env, error) {
|
||||
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),
|
||||
@@ -34,37 +28,7 @@ func NewEnvironment() (*cel.Env, error) {
|
||||
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)
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -24,11 +24,13 @@ type ParsedConfig struct {
|
||||
Bots []Bot
|
||||
DNSBL bool
|
||||
DefaultDifficulty int
|
||||
StatusCodes config.StatusCodes
|
||||
}
|
||||
|
||||
func NewParsedConfig(orig *config.Config) *ParsedConfig {
|
||||
return &ParsedConfig{
|
||||
orig: orig,
|
||||
orig: orig,
|
||||
StatusCodes: orig.StatusCodes,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
lib/testdata/aggressive_403.yaml
vendored
Normal file
12
lib/testdata/aggressive_403.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
bots:
|
||||
- name: deny
|
||||
user_agent_regex: DENY
|
||||
action: DENY
|
||||
|
||||
- name: challenge
|
||||
user_agent_regex: CHALLENGE
|
||||
action: CHALLENGE
|
||||
|
||||
status_codes:
|
||||
CHALLENGE: 401
|
||||
DENY: 403
|
||||
54
package-lock.json
generated
54
package-lock.json
generated
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"cssnano": "^7.0.6",
|
||||
"cssnano-preset-advanced": "^7.0.6",
|
||||
"esbuild": "^0.25.3",
|
||||
"playwright": "^1.52.0",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
@@ -1490,6 +1491,53 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.52.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
||||
@@ -2691,4 +2739,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -10,7 +10,8 @@
|
||||
"build": "npm run assets && go build -o ./var/anubis ./cmd/anubis",
|
||||
"dev": "npm run assets && go run ./cmd/anubis --use-remote-address",
|
||||
"container": "npm run assets && go run ./cmd/containerbuild",
|
||||
"package": "yeet"
|
||||
"package": "yeet",
|
||||
"lint": "make lint"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@@ -18,6 +19,7 @@
|
||||
"cssnano": "^7.0.6",
|
||||
"cssnano-preset-advanced": "^7.0.6",
|
||||
"esbuild": "^0.25.3",
|
||||
"playwright": "^1.52.0",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
|
||||
12
test/anubis_configs/aggressive_403.yaml
Normal file
12
test/anubis_configs/aggressive_403.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
bots:
|
||||
- name: deny
|
||||
user_agent_regex: DENY
|
||||
action: DENY
|
||||
|
||||
- name: challenge
|
||||
user_agent_regex: CHALLENGE
|
||||
action: CHALLENGE
|
||||
|
||||
status_codes:
|
||||
CHALLENGE: 401
|
||||
DENY: 403
|
||||
@@ -1,2 +0,0 @@
|
||||
bots:
|
||||
- import: (data)/common/challenge-browser-like.yaml
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -1,15 +0,0 @@
|
||||
<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>
|
||||
@@ -1,22 +0,0 @@
|
||||
<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 +0,0 @@
|
||||
Listen 443 https
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
@@ -1,3 +0,0 @@
|
||||
RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR}
|
||||
RequestHeader set "X-Forwarded-Proto" "https"
|
||||
RequestHeader set "X-Http-Version" "%{SERVER_PROTOCOL}s"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/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
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
- "../pki/caddy.local.cetacean.club:/etc/techaro/pki/caddy.local.cetacean.club/"
|
||||
|
||||
anubis:
|
||||
image: git.xeserv.us/techaro/anubis:cel
|
||||
image: ghcr.io/techarohq/anubis:main
|
||||
environment:
|
||||
BIND: ":3000"
|
||||
TARGET: http://httpdebug:3000
|
||||
|
||||
0
test/caddy/start.sh
Executable file → Normal file
0
test/caddy/start.sh
Executable file → Normal file
@@ -1,4 +0,0 @@
|
||||
FROM nginx
|
||||
|
||||
COPY conf.d/ /etc/nginx/conf.d/
|
||||
COPY snippets /etc/nginx/snippets
|
||||
@@ -1,10 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name nginx.local.cetacean.club;
|
||||
|
||||
location / {
|
||||
proxy_pass http://anubis:3000;
|
||||
include snippets/proxy_params;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
@@ -1,7 +0,0 @@
|
||||
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;
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/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
|
||||
@@ -37,6 +37,7 @@ go run ../cmd/unixhttpd &
|
||||
go tool anubis \
|
||||
--bind=./anubis.sock \
|
||||
--bind-network=unix \
|
||||
--policy-fname=../anubis_configs/aggressive_403.yaml \
|
||||
--target=unix://$(pwd)/unixhttpd.sock &
|
||||
|
||||
# A simple TLS terminator that forwards to Anubis, which will forward to
|
||||
|
||||
30
test/unix-socket-xff/test.mjs
Normal file
30
test/unix-socket-xff/test.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
async function testWithUserAgent(userAgent) {
|
||||
const statusCode =
|
||||
await fetch("https://relayd.local.cetacean.club:3004/reqmeta", {
|
||||
headers: {
|
||||
"User-Agent": userAgent,
|
||||
}
|
||||
})
|
||||
.then(resp => resp.status);
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
const codes = {
|
||||
allow: await testWithUserAgent("ALLOW"),
|
||||
challenge: await testWithUserAgent("CHALLENGE"),
|
||||
deny: await testWithUserAgent("DENY")
|
||||
}
|
||||
|
||||
const expected = {
|
||||
allow: 200,
|
||||
challenge: 401,
|
||||
deny: 403,
|
||||
};
|
||||
|
||||
console.log("ALLOW: ", codes.allow);
|
||||
console.log("CHALLENGE:", codes.challenge);
|
||||
console.log("DENY: ", codes.deny);
|
||||
|
||||
if (JSON.stringify(codes) !== JSON.stringify(expected)) {
|
||||
throw new Error(`wanted ${JSON.stringify(expected)}, got: ${JSON.stringify(codes)}`);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ for the JavaScript code in this page.
|
||||
*/'
|
||||
|
||||
esbuild js/main.mjs --sourcemap --bundle --minify --outfile=static/js/main.mjs "--banner:js=${LICENSE}"
|
||||
gzip -f -k static/js/main.mjs
|
||||
gzip -f -k -n static/js/main.mjs
|
||||
zstd -f -k --ultra -22 static/js/main.mjs
|
||||
brotli -fZk static/js/main.mjs
|
||||
|
||||
|
||||
2
web/index_templ.go
generated
2
web/index_templ.go
generated
@@ -1,6 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.857
|
||||
// templ: version: v0.3.865
|
||||
package web
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
@@ -28,6 +28,11 @@ const dependencies = [
|
||||
msg: "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?",
|
||||
value: window.Worker,
|
||||
},
|
||||
{
|
||||
name: "Cookies",
|
||||
msg: "Your browser doesn't store cookies. Anubis uses cookies to determine which clients have passed challenges by storing a signed token in a cookie. Please enable storing cookies for this domain. The names of the cookies Anubis stores may vary without notice. Cookie names and values are not part of the public API.",
|
||||
value: navigator.cookieEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
function showContinueBar(hash, nonce, t0, t1) {
|
||||
@@ -131,6 +136,7 @@ function showContinueBar(hash, nonce, t0, t1) {
|
||||
statusMsg: msg,
|
||||
imageSrc: imageURL("reject", anubisVersion, basePrefix),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
web/static/img/ATTRIBUTIONS.txt
Normal file
3
web/static/img/ATTRIBUTIONS.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
These mascot images were made by CELPHASE (https://bsky.app/profile/celphase.bsky.social).
|
||||
|
||||
These images are available under the terms of the MIT license that this repository uses.
|
||||
@@ -3,25 +3,28 @@
|
||||
--body-preformatted-font: Iosevka Curly Iaso, monospace;
|
||||
--body-title-font: Podkova, serif;
|
||||
|
||||
--dark-background: #1d2021;
|
||||
--dark-text: #f9f5d7;
|
||||
--dark-text-selection: #d3869b;
|
||||
--dark-preformatted-background: #3c3836;
|
||||
--dark-link-foreground: #b16286;
|
||||
--dark-link-background: #282828;
|
||||
--dark-blockquote-border-left: 1px solid #bdae93;
|
||||
|
||||
--light-background: #f9f5d7;
|
||||
--light-text: #1d2021;
|
||||
--light-text-selection: #d3869b;
|
||||
--light-preformatted-background: #ebdbb2;
|
||||
--light-link-foreground: #b16286;
|
||||
--light-link-background: #fbf1c7;
|
||||
--light-blockquote-border-left: 1px solid #655c54;
|
||||
--background: #1d2021;
|
||||
--text: #f9f5d7;
|
||||
--text-selection: #d3869b;
|
||||
--preformatted-background: #3c3836;
|
||||
--link-foreground: #b16286;
|
||||
--link-background: #282828;
|
||||
--blockquote-border-left: 1px solid #bdae93;
|
||||
|
||||
--progress-bar-outline: #b16286 solid 4px;
|
||||
--progress-bar-fill: #b16286;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--background: #f9f5d7;
|
||||
--text: #1d2021;
|
||||
--text-selection: #d3869b;
|
||||
--preformatted-background: #ebdbb2;
|
||||
--link-foreground: #b16286;
|
||||
--link-background: #fbf1c7;
|
||||
--blockquote-border-left: 1px solid #655c54;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Geist";
|
||||
@@ -54,12 +57,12 @@ main {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--dark-text-selection);
|
||||
background: var(--text-selection);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--dark-background);
|
||||
color: var(--dark-text);
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
body,
|
||||
@@ -113,7 +116,7 @@ html {
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--dark-preformatted-background);
|
||||
background-color: var(--preformatted-background);
|
||||
padding: 1em;
|
||||
border: 0;
|
||||
font-family: var(--body-preformatted-font);
|
||||
@@ -122,8 +125,8 @@ pre {
|
||||
a,
|
||||
a:active,
|
||||
a:visited {
|
||||
color: var(--dark-link-foreground);
|
||||
background-color: var(--dark-link-background);
|
||||
color: var(--link-foreground);
|
||||
background-color: var(--link-background);
|
||||
}
|
||||
|
||||
h1,
|
||||
@@ -136,7 +139,7 @@ h5 {
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: var(--dark-blockquote-border-left);
|
||||
border-left: var(--blockquote-border-left);
|
||||
margin: 0.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
}
|
||||
@@ -144,41 +147,3 @@ blockquote {
|
||||
footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
::selection {
|
||||
background: var(--light-text-selection);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--light-background);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--light-preformatted-background);
|
||||
padding: 1em;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a,
|
||||
a:active,
|
||||
a:visited {
|
||||
color: var(--light-link-foreground);
|
||||
background-color: var(--light-link-background);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: var(--light-blockquote-border-left);
|
||||
margin: 0.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user