mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-05 16:28:17 +00:00
Compare commits
73 Commits
Xe/valkey
...
Xe/docker-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50e48df993 | ||
|
|
5e38c7d730 | ||
|
|
c638653172 | ||
|
|
0fe46b48cf | ||
|
|
d6e5561768 | ||
|
|
6594ae0eef | ||
|
|
ad09f82c3c | ||
|
|
372b797f64 | ||
|
|
6eaf0e13a2 | ||
|
|
281b6c5c00 | ||
|
|
9539668049 | ||
|
|
8eff57fcb6 | ||
|
|
4ac59c3a79 | ||
|
|
bee1c22b96 | ||
|
|
5a7499ea3b | ||
|
|
5f3861ab37 | ||
|
|
9f1d791991 | ||
|
|
76fa3e01a5 | ||
|
|
f2db43ad4b | ||
|
|
ba4412c907 | ||
|
|
f184cd81e7 | ||
|
|
59bfced8bf | ||
|
|
780a935cb8 | ||
|
|
f4bc1df797 | ||
|
|
b496c90e86 | ||
|
|
ec73bcbaf1 | ||
|
|
8d19eed200 | ||
|
|
ec733e93a5 | ||
|
|
51c384eefd | ||
|
|
44d5ec0b6e | ||
|
|
3bc9040a96 | ||
|
|
de7dbfe6d6 | ||
|
|
77e0bbbce9 | ||
|
|
b4b5d2f82e | ||
|
|
988fff77f1 | ||
|
|
0d9ebebff6 | ||
|
|
ba00cdacd2 | ||
|
|
68a71c6a99 | ||
|
|
fbbab5a035 | ||
|
|
28ab29389c | ||
|
|
497005ce3e | ||
|
|
669eb4ba4b | ||
|
|
6c4e739b0b | ||
|
|
c8635357dc | ||
|
|
0ed905fd4e | ||
|
|
cd8a7eb2e2 | ||
|
|
22c47f40d1 | ||
|
|
669671bd46 | ||
|
|
6c247cdec8 | ||
|
|
eeae28f459 | ||
|
|
9ba10262e3 | ||
|
|
a28a3d155a | ||
|
|
086f43e3ca | ||
|
|
fa1f2355ea | ||
|
|
0a56194825 | ||
|
|
93e2447ba2 | ||
|
|
51f875ff6f | ||
|
|
555a188dc3 | ||
|
|
6f08bcb481 | ||
|
|
11081aac08 | ||
|
|
c78d830ecb | ||
|
|
5e7bfa5ec2 | ||
|
|
7b8953303d | ||
|
|
a6045d6698 | ||
|
|
e31e1ca5e7 | ||
|
|
50e030d17e | ||
|
|
b640c567da | ||
|
|
9e9982ab5d | ||
|
|
3b98368aa9 | ||
|
|
76849531cd | ||
|
|
961320540b | ||
|
|
91c21fbb4b | ||
|
|
caf69be97b |
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.env
|
||||||
|
*.deb
|
||||||
|
*.rpm
|
||||||
|
|
||||||
|
# Additional package locks
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Go binaries and test artifacts
|
||||||
|
main
|
||||||
|
*.test
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_store
|
||||||
|
|
||||||
|
# Intellij
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# how does this get here
|
||||||
|
doc/VERSION
|
||||||
|
|
||||||
|
web/static/js/*
|
||||||
|
!web/static/js/.gitignore
|
||||||
68
.github/actions/spelling/expect.txt
vendored
68
.github/actions/spelling/expect.txt
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
acs
|
||||||
aeacus
|
aeacus
|
||||||
Aibrew
|
Aibrew
|
||||||
alrest
|
alrest
|
||||||
@@ -5,38 +6,46 @@ amazonbot
|
|||||||
anthro
|
anthro
|
||||||
anubis
|
anubis
|
||||||
anubistest
|
anubistest
|
||||||
|
Applebot
|
||||||
archlinux
|
archlinux
|
||||||
badregexes
|
badregexes
|
||||||
|
bdba
|
||||||
berr
|
berr
|
||||||
bingbot
|
bingbot
|
||||||
Bitcoin
|
bitcoin
|
||||||
blogging
|
blogging
|
||||||
Bluesky
|
Bluesky
|
||||||
blueskybot
|
blueskybot
|
||||||
boi
|
boi
|
||||||
botnet
|
botnet
|
||||||
BPort
|
BPort
|
||||||
|
Brightbot
|
||||||
broked
|
broked
|
||||||
|
Bytespider
|
||||||
cachebuster
|
cachebuster
|
||||||
Caddyfile
|
Caddyfile
|
||||||
caninetools
|
caninetools
|
||||||
Cardyb
|
Cardyb
|
||||||
celchecker
|
celchecker
|
||||||
CELPHASE
|
CELPHASE
|
||||||
|
cerr
|
||||||
certresolver
|
certresolver
|
||||||
CGNAT
|
CGNAT
|
||||||
cgr
|
cgr
|
||||||
chainguard
|
chainguard
|
||||||
chall
|
chall
|
||||||
challengemozilla
|
challengemozilla
|
||||||
|
checkpath
|
||||||
checkresult
|
checkresult
|
||||||
chen
|
chen
|
||||||
chibi
|
chibi
|
||||||
cidranger
|
cidranger
|
||||||
ckie
|
ckie
|
||||||
cloudflare
|
cloudflare
|
||||||
|
confd
|
||||||
containerbuild
|
containerbuild
|
||||||
coreutils
|
coreutils
|
||||||
|
Cotoyogi
|
||||||
CRDs
|
CRDs
|
||||||
crt
|
crt
|
||||||
daemonizing
|
daemonizing
|
||||||
@@ -45,6 +54,7 @@ Debian
|
|||||||
debrpm
|
debrpm
|
||||||
decaymap
|
decaymap
|
||||||
decompiling
|
decompiling
|
||||||
|
Diffbot
|
||||||
discordapp
|
discordapp
|
||||||
discordbot
|
discordbot
|
||||||
distros
|
distros
|
||||||
@@ -55,17 +65,22 @@ dracula
|
|||||||
dronebl
|
dronebl
|
||||||
droneblresponse
|
droneblresponse
|
||||||
duckduckbot
|
duckduckbot
|
||||||
|
eerror
|
||||||
ellenjoe
|
ellenjoe
|
||||||
enbyware
|
enbyware
|
||||||
everyones
|
everyones
|
||||||
evilbot
|
evilbot
|
||||||
evilsite
|
evilsite
|
||||||
expressionorlist
|
expressionorlist
|
||||||
|
externalagent
|
||||||
|
externalfetcher
|
||||||
extldflags
|
extldflags
|
||||||
facebookgo
|
facebookgo
|
||||||
|
Factset
|
||||||
fastcgi
|
fastcgi
|
||||||
fediverse
|
fediverse
|
||||||
finfos
|
finfos
|
||||||
|
Firecrawl
|
||||||
flagenv
|
flagenv
|
||||||
Fordola
|
Fordola
|
||||||
forgejo
|
forgejo
|
||||||
@@ -80,19 +95,30 @@ goodbot
|
|||||||
googlebot
|
googlebot
|
||||||
govulncheck
|
govulncheck
|
||||||
GPG
|
GPG
|
||||||
|
GPT
|
||||||
|
gptbot
|
||||||
|
grw
|
||||||
Hashcash
|
Hashcash
|
||||||
hashrate
|
hashrate
|
||||||
headermap
|
headermap
|
||||||
healthcheck
|
healthcheck
|
||||||
|
hebis
|
||||||
hec
|
hec
|
||||||
hmc
|
hmc
|
||||||
hostable
|
hostable
|
||||||
|
htmx
|
||||||
httpdebug
|
httpdebug
|
||||||
|
hypertext
|
||||||
|
iaskspider
|
||||||
iat
|
iat
|
||||||
ifm
|
ifm
|
||||||
|
Imagesift
|
||||||
|
imgproxy
|
||||||
inp
|
inp
|
||||||
iss
|
iss
|
||||||
|
isset
|
||||||
ivh
|
ivh
|
||||||
|
Jenomis
|
||||||
JGit
|
JGit
|
||||||
journalctl
|
journalctl
|
||||||
jshelter
|
jshelter
|
||||||
@@ -104,34 +130,49 @@ keypair
|
|||||||
KHTML
|
KHTML
|
||||||
kinda
|
kinda
|
||||||
KUBECONFIG
|
KUBECONFIG
|
||||||
|
lcj
|
||||||
ldflags
|
ldflags
|
||||||
letsencrypt
|
letsencrypt
|
||||||
|
Lexentale
|
||||||
lgbt
|
lgbt
|
||||||
licend
|
licend
|
||||||
licstart
|
licstart
|
||||||
lightpanda
|
lightpanda
|
||||||
|
LIMSA
|
||||||
Linting
|
Linting
|
||||||
linuxbrew
|
linuxbrew
|
||||||
LLU
|
LLU
|
||||||
loadbalancer
|
loadbalancer
|
||||||
lol
|
lol
|
||||||
|
LOMINSA
|
||||||
maintainership
|
maintainership
|
||||||
malware
|
malware
|
||||||
mcr
|
mcr
|
||||||
memes
|
memes
|
||||||
|
metarefresh
|
||||||
|
metrix
|
||||||
mimi
|
mimi
|
||||||
minica
|
minica
|
||||||
|
mistralai
|
||||||
Mojeek
|
Mojeek
|
||||||
mojeekbot
|
mojeekbot
|
||||||
mozilla
|
mozilla
|
||||||
nbf
|
nbf
|
||||||
|
netsurf
|
||||||
nginx
|
nginx
|
||||||
nobots
|
nobots
|
||||||
NONINFRINGEMENT
|
NONINFRINGEMENT
|
||||||
nosleep
|
nosleep
|
||||||
|
OCOB
|
||||||
ogtags
|
ogtags
|
||||||
|
omgili
|
||||||
|
omgilibot
|
||||||
onionservice
|
onionservice
|
||||||
|
openai
|
||||||
|
openrc
|
||||||
pag
|
pag
|
||||||
|
palemoon
|
||||||
|
Pangu
|
||||||
parseable
|
parseable
|
||||||
passthrough
|
passthrough
|
||||||
Patreon
|
Patreon
|
||||||
@@ -147,8 +188,10 @@ prebaked
|
|||||||
privkey
|
privkey
|
||||||
promauto
|
promauto
|
||||||
promhttp
|
promhttp
|
||||||
|
proofofwork
|
||||||
pwcmd
|
pwcmd
|
||||||
pwuser
|
pwuser
|
||||||
|
qualys
|
||||||
qwant
|
qwant
|
||||||
qwantbot
|
qwantbot
|
||||||
rac
|
rac
|
||||||
@@ -160,17 +203,27 @@ reputational
|
|||||||
reqmeta
|
reqmeta
|
||||||
risc
|
risc
|
||||||
ruleset
|
ruleset
|
||||||
|
runlevels
|
||||||
RUnlock
|
RUnlock
|
||||||
sas
|
sas
|
||||||
|
sasl
|
||||||
Scumm
|
Scumm
|
||||||
|
searchbot
|
||||||
|
searx
|
||||||
sebest
|
sebest
|
||||||
secretplans
|
secretplans
|
||||||
selfsigned
|
selfsigned
|
||||||
|
Semrush
|
||||||
setsebool
|
setsebool
|
||||||
|
shellcheck
|
||||||
|
Sidetrade
|
||||||
sitemap
|
sitemap
|
||||||
|
sls
|
||||||
|
sni
|
||||||
Sourceware
|
Sourceware
|
||||||
Spambot
|
Spambot
|
||||||
sparkline
|
sparkline
|
||||||
|
spyderbot
|
||||||
srv
|
srv
|
||||||
stackoverflow
|
stackoverflow
|
||||||
startprecmd
|
startprecmd
|
||||||
@@ -178,6 +231,7 @@ stoppostcmd
|
|||||||
subgrid
|
subgrid
|
||||||
subr
|
subr
|
||||||
subrequest
|
subrequest
|
||||||
|
SVCNAME
|
||||||
tagline
|
tagline
|
||||||
tarballs
|
tarballs
|
||||||
techaro
|
techaro
|
||||||
@@ -185,12 +239,17 @@ techarohq
|
|||||||
templ
|
templ
|
||||||
templruntime
|
templruntime
|
||||||
testarea
|
testarea
|
||||||
|
Tik
|
||||||
|
Timpibot
|
||||||
torproject
|
torproject
|
||||||
traefik
|
traefik
|
||||||
|
uberspace
|
||||||
unixhttpd
|
unixhttpd
|
||||||
unmarshal
|
unmarshal
|
||||||
uvx
|
uvx
|
||||||
|
UXP
|
||||||
Varis
|
Varis
|
||||||
|
Velen
|
||||||
vendored
|
vendored
|
||||||
vhosts
|
vhosts
|
||||||
videotest
|
videotest
|
||||||
@@ -200,8 +259,12 @@ webmaster
|
|||||||
webpage
|
webpage
|
||||||
websecure
|
websecure
|
||||||
websites
|
websites
|
||||||
workaround
|
Webzio
|
||||||
|
wildbase
|
||||||
|
wordpress
|
||||||
|
Workaround
|
||||||
workdir
|
workdir
|
||||||
|
wpbot
|
||||||
xcaddy
|
xcaddy
|
||||||
Xeact
|
Xeact
|
||||||
xeiaso
|
xeiaso
|
||||||
@@ -210,6 +273,7 @@ xesite
|
|||||||
xess
|
xess
|
||||||
xff
|
xff
|
||||||
XForwarded
|
XForwarded
|
||||||
|
XNG
|
||||||
XReal
|
XReal
|
||||||
yae
|
yae
|
||||||
YAMLTo
|
YAMLTo
|
||||||
|
|||||||
@@ -20,9 +20,6 @@
|
|||||||
# https://twitter.com/nyttypos/status/1898844061873639490
|
# https://twitter.com/nyttypos/status/1898844061873639490
|
||||||
#\([A-Z][a-z]{2,}(?: [a-z]+){3,}\)\.\s
|
#\([A-Z][a-z]{2,}(?: [a-z]+){3,}\)\.\s
|
||||||
|
|
||||||
# Complete sentences shouldn't be in the middle of another sentence as a parenthetical.
|
|
||||||
(?<!\.)\.\),
|
|
||||||
|
|
||||||
# Complete sentences in parentheticals should not have a space before the period.
|
# Complete sentences in parentheticals should not have a space before the period.
|
||||||
\s\.\)(?!.*\}\})
|
\s\.\)(?!.*\}\})
|
||||||
|
|
||||||
|
|||||||
4
.github/actions/spelling/patterns.txt
vendored
4
.github/actions/spelling/patterns.txt
vendored
@@ -128,3 +128,7 @@ go install(?:\s+[a-z]+\.[-@\w/.]+)+
|
|||||||
|
|
||||||
# ignore long runs of a single character:
|
# ignore long runs of a single character:
|
||||||
\b([A-Za-z])\g{-1}{3,}\b
|
\b([A-Za-z])\g{-1}{3,}\b
|
||||||
|
|
||||||
|
# hit-count: 1 file-count: 1
|
||||||
|
# microsoft
|
||||||
|
\b(?:https?://|)(?:(?:(?:blogs|download\.visualstudio|docs|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%]*
|
||||||
14
.github/workflows/docs-deploy.yml
vendored
14
.github/workflows/docs-deploy.yml
vendored
@@ -3,7 +3,7 @@ name: Docs deploy
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ["main"]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
|
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./docs
|
context: ./docs
|
||||||
cache-to: type=gha
|
cache-to: type=gha
|
||||||
@@ -50,15 +50,15 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
|
|
||||||
- name: Apply k8s manifests to aeacus
|
- name: Apply k8s manifests to aeacus
|
||||||
uses: actions-hub/kubectl@e81783053d902f50d752d21a6d99cf9689a652e1 # v1.33.0
|
uses: actions-hub/kubectl@f632a31512a74cb35940627c49c20f67723cbaaf # v1.33.1
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG: ${{ secrets.AEACUS_KUBECONFIG }}
|
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||||
with:
|
with:
|
||||||
args: apply -k docs/manifest
|
args: apply -k docs/manifest
|
||||||
|
|
||||||
- name: Apply k8s manifests to aeacus
|
- name: Apply k8s manifests to aeacus
|
||||||
uses: actions-hub/kubectl@e81783053d902f50d752d21a6d99cf9689a652e1 # v1.33.0
|
uses: actions-hub/kubectl@f632a31512a74cb35940627c49c20f67723cbaaf # v1.33.1
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG: ${{ secrets.AEACUS_KUBECONFIG }}
|
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||||
with:
|
with:
|
||||||
args: rollout restart -n default deploy/anubis-docs
|
args: rollout restart -n default deploy/anubis-docs
|
||||||
|
|||||||
2
.github/workflows/docs-test.yml
vendored
2
.github/workflows/docs-test.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./docs
|
context: ./docs
|
||||||
cache-to: type=gha
|
cache-to: type=gha
|
||||||
|
|||||||
2
.github/workflows/spelling.yml
vendored
2
.github/workflows/spelling.yml
vendored
@@ -89,7 +89,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: check-spelling
|
- name: check-spelling
|
||||||
id: spelling
|
id: spelling
|
||||||
uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
|
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
|
||||||
with:
|
with:
|
||||||
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
|
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
|
||||||
checkout: true
|
checkout: true
|
||||||
|
|||||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
|
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
run: uvx zizmor --format sarif . > results.sarif
|
run: uvx zizmor --format sarif . > results.sarif
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
ARG ALPINE_VERSION=edge
|
||||||
|
FROM --platform=${BUILDPLATFORM} alpine:${ALPINE_VERSION} AS build
|
||||||
|
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG COMPONENT=anubis
|
||||||
|
ARG VERSION=devel-docker
|
||||||
|
|
||||||
|
RUN apk -U add go nodejs git build-base git npm bash zstd brotli gzip
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache npm ci && npm run assets
|
||||||
|
RUN --mount=type=cache,target=/root/.cache GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 GOARM=7 go build -gcflags "all=-N -l" -o /app/bin/${COMPONENT} -ldflags "-s -w -extldflags -static -X github.com/TecharoHQ/anubis.Version=${VERSION}" ./cmd/${COMPONENT}
|
||||||
|
|
||||||
|
FROM alpine:${ALPINE_VERSION} AS run
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk -U add ca-certificates mailcap
|
||||||
|
|
||||||
|
COPY --from=build /app/bin/anubis /app/bin/anubis
|
||||||
|
|
||||||
|
CMD ["/app/bin/anubis"]
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/app/bin/anubis", "--healthcheck" ]
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
||||||
32
README.md
32
README.md
@@ -14,14 +14,36 @@
|
|||||||
|
|
||||||
Anubis is brought to you by sponsors and donors like:
|
Anubis is brought to you by sponsors and donors like:
|
||||||
|
|
||||||
[](https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis)
|
### Diamond Tier
|
||||||
[](https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh)
|
|
||||||
[](https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis)
|
<a href="https://www.raptorcs.com/content/base/products.html">
|
||||||
[](https://weblate.org/?utm_campaign=github&utm_medium=referral&utm_content=anubis)
|
<img src="./docs/static/img/sponsors/raptor-computing-logo.webp" alt="Raptor Computing Systems" height=64 />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### Gold Tier
|
||||||
|
|
||||||
|
<a href="https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img src="./docs/static/img/sponsors/distrust-logo.webp" alt="Distrust" height="64">
|
||||||
|
</a>
|
||||||
|
<a href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh">
|
||||||
|
<img src="./docs/static/img/sponsors/terminal-trove.webp" alt="Terminal Trove" height="64">
|
||||||
|
</a>
|
||||||
|
<a href="https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img src="./docs/static/img/sponsors/caninetools-logo.webp" alt="canine.tools" height="64">
|
||||||
|
</a>
|
||||||
|
<a href="https://weblate.org/">
|
||||||
|
<img src="./docs/static/img/sponsors/weblate-logo.webp" alt="Weblate" height="64">
|
||||||
|
</a>
|
||||||
|
<a href="https://uberspace.de/">
|
||||||
|
<img src="./docs/static/img/sponsors/uberspace-logo.webp" alt="Uberspace" height="64">
|
||||||
|
</a>
|
||||||
|
<a href="https://wildbase.xyz/">
|
||||||
|
<img src="./docs/static/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64">
|
||||||
|
</a>
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Anubis [weighs the soul of your connection](https://en.wikipedia.org/wiki/Weighing_of_souls) using a proof-of-work challenge in order to protect upstream resources from scraper bots.
|
Anubis is a Web AI Firewall Utility that [weighs the soul of your connection](https://en.wikipedia.org/wiki/Weighing_of_souls) using one or more challenges in order to protect upstream resources from scraper bots.
|
||||||
|
|
||||||
This program is designed to help protect the small internet from the endless storm of requests that flood in from AI companies. Anubis is as lightweight as possible to ensure that everyone can afford to protect the communities closest to them.
|
This program is designed to help protect the small internet from the endless storm of requests that flood in from AI companies. Anubis is as lightweight as possible to ensure that everyone can afford to protect the communities closest to them.
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const CookieName = "techaro.lol-anubis-auth"
|
|||||||
// WithDomainCookieName is the name that is prepended to the per-domain cookie used when COOKIE_DOMAIN is set.
|
// WithDomainCookieName is the name that is prepended to the per-domain cookie used when COOKIE_DOMAIN is set.
|
||||||
const WithDomainCookieName = "techaro.lol-anubis-auth-for-"
|
const WithDomainCookieName = "techaro.lol-anubis-auth-for-"
|
||||||
|
|
||||||
|
const TestCookieName = "techaro.lol-anubis-cookie-test-if-you-block-this-anubis-wont-work"
|
||||||
|
|
||||||
// CookieDefaultExpirationTime is the amount of time before the cookie/JWT expires.
|
// CookieDefaultExpirationTime is the amount of time before the cookie/JWT expires.
|
||||||
const CookieDefaultExpirationTime = 7 * 24 * time.Hour
|
const CookieDefaultExpirationTime = 7 * 24 * time.Hour
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ var (
|
|||||||
redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
|
redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
|
||||||
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
||||||
target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request")
|
target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request")
|
||||||
|
targetSNI = flag.String("target-sni", "", "if set, the value of the TLS handshake hostname when forwarding requests to the target")
|
||||||
|
targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target")
|
||||||
targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend")
|
targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend")
|
||||||
healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
|
healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
|
||||||
useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal")
|
useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal")
|
||||||
@@ -65,6 +67,8 @@ var (
|
|||||||
ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache")
|
ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache")
|
||||||
extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
|
extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
|
||||||
webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
|
webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
|
||||||
|
versionFlag = flag.Bool("version", false, "print Anubis version")
|
||||||
|
xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For")
|
||||||
)
|
)
|
||||||
|
|
||||||
func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
||||||
@@ -135,7 +139,7 @@ func setupListener(network string, address string) (net.Listener, string) {
|
|||||||
return listener, formattedAddress
|
return listener, formattedAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeReverseProxy(target string, insecureSkipVerify bool) (http.Handler, error) {
|
func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool) (http.Handler, error) {
|
||||||
targetUri, err := url.Parse(target)
|
targetUri, err := url.Parse(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse target URL: %w", err)
|
return nil, fmt.Errorf("failed to parse target URL: %w", err)
|
||||||
@@ -157,16 +161,28 @@ func makeReverseProxy(target string, insecureSkipVerify bool) (http.Handler, err
|
|||||||
transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport})
|
transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport})
|
||||||
}
|
}
|
||||||
|
|
||||||
if insecureSkipVerify {
|
if insecureSkipVerify || targetSNI != "" {
|
||||||
slog.Warn("TARGET_INSECURE_SKIP_VERIFY is set to true, TLS certificate validation will not be performed", "target", target)
|
transport.TLSClientConfig = &tls.Config{}
|
||||||
transport.TLSClientConfig = &tls.Config{
|
if insecureSkipVerify {
|
||||||
InsecureSkipVerify: true,
|
slog.Warn("TARGET_INSECURE_SKIP_VERIFY is set to true, TLS certificate validation will not be performed", "target", target)
|
||||||
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||||
|
}
|
||||||
|
if targetSNI != "" {
|
||||||
|
transport.TLSClientConfig.ServerName = targetSNI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rp := httputil.NewSingleHostReverseProxy(targetUri)
|
rp := httputil.NewSingleHostReverseProxy(targetUri)
|
||||||
rp.Transport = transport
|
rp.Transport = transport
|
||||||
|
|
||||||
|
if targetHost != "" {
|
||||||
|
originalDirector := rp.Director
|
||||||
|
rp.Director = func(req *http.Request) {
|
||||||
|
originalDirector(req)
|
||||||
|
req.Host = targetHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rp, nil
|
return rp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +204,11 @@ func main() {
|
|||||||
flagenv.Parse()
|
flagenv.Parse()
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *versionFlag {
|
||||||
|
fmt.Println("Anubis", anubis.Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
internal.InitSlog(*slogLevel)
|
internal.InitSlog(*slogLevel)
|
||||||
|
|
||||||
if *extractResources != "" {
|
if *extractResources != "" {
|
||||||
@@ -205,7 +226,7 @@ func main() {
|
|||||||
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
|
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
|
||||||
if strings.TrimSpace(*target) != "" {
|
if strings.TrimSpace(*target) != "" {
|
||||||
var err error
|
var err error
|
||||||
rp, err = makeReverseProxy(*target, *targetInsecureSkipVerify)
|
rp, err = makeReverseProxy(*target, *targetSNI, *targetHost, *targetInsecureSkipVerify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("can't make reverse proxy: %v", err)
|
log.Fatalf("can't make reverse proxy: %v", err)
|
||||||
}
|
}
|
||||||
@@ -316,7 +337,7 @@ func main() {
|
|||||||
h = s
|
h = s
|
||||||
h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
|
h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
|
||||||
h = internal.XForwardedForToXRealIP(h)
|
h = internal.XForwardedForToXRealIP(h)
|
||||||
h = internal.XForwardedForUpdate(h)
|
h = internal.XForwardedForUpdate(*xffStripPrivate, h)
|
||||||
|
|
||||||
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||||
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
||||||
@@ -400,11 +421,11 @@ func extractEmbedFS(fsys embed.FS, root string, destDir string) error {
|
|||||||
return os.MkdirAll(destPath, 0o700)
|
return os.MkdirAll(destPath, 0o700)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := fs.ReadFile(fsys, path)
|
embeddedData, err := fs.ReadFile(fsys, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(destPath, data, 0o644)
|
return os.WriteFile(destPath, embeddedData, 0o644)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
20
data/apps/bookstack-saml.yaml
Normal file
20
data/apps/bookstack-saml.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Make SASL login work on bookstack with Anubis
|
||||||
|
# https://www.bookstackapp.com/docs/admin/saml2-auth/
|
||||||
|
- name: allow-bookstack-sasl-login-routes
|
||||||
|
action: ALLOW
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- 'method == "POST"'
|
||||||
|
- path.startsWith("/saml2/acs")
|
||||||
|
- name: allow-bookstack-sasl-metadata-routes
|
||||||
|
action: ALLOW
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- 'method == "GET"'
|
||||||
|
- path.startsWith("/saml2/metadata")
|
||||||
|
- name: allow-bookstack-sasl-logout-routes
|
||||||
|
action: ALLOW
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- 'method == "GET"'
|
||||||
|
- path.startsWith("/saml2/sls")
|
||||||
7
data/apps/qualys-ssl-labs.yml
Normal file
7
data/apps/qualys-ssl-labs.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# This policy allows Qualys SSL Labs to fully work. (https://www.ssllabs.com/ssltest)
|
||||||
|
# IP ranges are taken from: https://qualys.my.site.com/discussions/s/article/000005823
|
||||||
|
- name: qualys-ssl-labs
|
||||||
|
action: ALLOW
|
||||||
|
remote_addresses:
|
||||||
|
- 64.41.200.0/24
|
||||||
|
- 2600:C02:1020:4202::/64
|
||||||
9
data/apps/searx-checker.yml
Normal file
9
data/apps/searx-checker.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# This policy allows SearXNG's instance tracker to work. (https://searx.space)
|
||||||
|
# IPs are taken from `check.searx.space` DNS records.
|
||||||
|
# https://toolbox.googleapps.com/apps/dig/#A/check.searx.space
|
||||||
|
# https://toolbox.googleapps.com/apps/dig/#AAAA/check.searx.space
|
||||||
|
- name: searx-checker
|
||||||
|
action: ALLOW
|
||||||
|
remote_addresses:
|
||||||
|
- 167.235.158.251/32
|
||||||
|
- 2a01:4f8:1c1c:8fc2::1/128
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"import": "(data)/bots/_deny-pathological.yaml"
|
"import": "(data)/bots/_deny-pathological.yaml"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"import": "(data)/bots/ai-robots-txt.yaml"
|
"import": "(data)/meta/ai-block-aggressive.yaml"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"import": "(data)/crawlers/_allow-good.yaml"
|
"import": "(data)/crawlers/_allow-good.yaml"
|
||||||
|
|||||||
@@ -11,44 +11,53 @@
|
|||||||
## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
|
## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
|
||||||
|
|
||||||
bots:
|
bots:
|
||||||
# Pathological bots to deny
|
# Pathological bots to deny
|
||||||
- # This correlates to data/bots/deny-pathological.yaml in the source tree
|
- # This correlates to data/bots/deny-pathological.yaml in the source tree
|
||||||
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/deny-pathological.yaml
|
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/deny-pathological.yaml
|
||||||
import: (data)/bots/_deny-pathological.yaml
|
import: (data)/bots/_deny-pathological.yaml
|
||||||
- import: (data)/bots/aggressive-brazilian-scrapers.yaml
|
- import: (data)/bots/aggressive-brazilian-scrapers.yaml
|
||||||
|
|
||||||
# Enforce https://github.com/ai-robots-txt/ai.robots.txt
|
# Aggressively block AI/LLM related bots/agents by default
|
||||||
- import: (data)/bots/ai-robots-txt.yaml
|
- import: (data)/meta/ai-block-aggressive.yaml
|
||||||
|
|
||||||
# Search engine crawlers to allow, defaults to:
|
# Consider replacing the aggressive AI policy with more selective policies:
|
||||||
# - Google (so they don't try to bypass Anubis)
|
# - import: (data)/meta/ai-block-moderate.yaml
|
||||||
# - Bing
|
# - import: (data)/meta/ai-block-permissive.yaml
|
||||||
# - DuckDuckGo
|
|
||||||
# - Qwant
|
|
||||||
# - The Internet Archive
|
|
||||||
# - Kagi
|
|
||||||
# - Marginalia
|
|
||||||
# - Mojeek
|
|
||||||
- import: (data)/crawlers/_allow-good.yaml
|
|
||||||
|
|
||||||
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
|
# Search engine crawlers to allow, defaults to:
|
||||||
- import: (data)/common/keep-internet-working.yaml
|
# - Google (so they don't try to bypass Anubis)
|
||||||
|
# - Apple
|
||||||
|
# - Bing
|
||||||
|
# - DuckDuckGo
|
||||||
|
# - Qwant
|
||||||
|
# - The Internet Archive
|
||||||
|
# - Kagi
|
||||||
|
# - Marginalia
|
||||||
|
# - Mojeek
|
||||||
|
- import: (data)/crawlers/_allow-good.yaml
|
||||||
|
# Challenge Firefox AI previews
|
||||||
|
- import: (data)/clients/x-firefox-ai.yaml
|
||||||
|
|
||||||
# # Punish any bot with "bot" in the user-agent string
|
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
|
||||||
# # This is known to have a high false-positive rate, use at your own risk
|
- import: (data)/common/keep-internet-working.yaml
|
||||||
# - name: generic-bot-catchall
|
|
||||||
# user_agent_regex: (?i:bot|crawler)
|
|
||||||
# action: CHALLENGE
|
|
||||||
# challenge:
|
|
||||||
# difficulty: 16 # impossible
|
|
||||||
# report_as: 4 # lie to the operator
|
|
||||||
# algorithm: slow # intentionally waste CPU cycles and time
|
|
||||||
|
|
||||||
# Generic catchall rule
|
# # Punish any bot with "bot" in the user-agent string
|
||||||
- name: generic-browser
|
# # This is known to have a high false-positive rate, use at your own risk
|
||||||
user_agent_regex: >-
|
# - name: generic-bot-catchall
|
||||||
Mozilla|Opera
|
# user_agent_regex: (?i:bot|crawler)
|
||||||
action: CHALLENGE
|
# action: CHALLENGE
|
||||||
|
# challenge:
|
||||||
|
# difficulty: 16 # impossible
|
||||||
|
# report_as: 4 # lie to the operator
|
||||||
|
# algorithm: slow # intentionally waste CPU cycles and time
|
||||||
|
|
||||||
|
# Generic catchall rule
|
||||||
|
- name: generic-browser
|
||||||
|
user_agent_regex: >-
|
||||||
|
Mozilla|Opera
|
||||||
|
action: WEIGH
|
||||||
|
weight:
|
||||||
|
adjust: 10
|
||||||
|
|
||||||
dnsbl: false
|
dnsbl: false
|
||||||
|
|
||||||
@@ -58,4 +67,4 @@ dnsbl: false
|
|||||||
# will stop sending requests once they get it.
|
# will stop sending requests once they get it.
|
||||||
status_codes:
|
status_codes:
|
||||||
CHALLENGE: 200
|
CHALLENGE: 200
|
||||||
DENY: 200
|
DENY: 200
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
- name: deny-aggressive-brazilian-scrapers
|
- name: deny-aggressive-brazilian-scrapers
|
||||||
action: DENY
|
action: WEIGH
|
||||||
|
weight:
|
||||||
|
adjust: 20
|
||||||
expression:
|
expression:
|
||||||
any:
|
any:
|
||||||
# Internet Explorer should be out of support
|
# Internet Explorer should be out of support
|
||||||
- userAgent.contains("MSIE")
|
- userAgent.contains("MSIE")
|
||||||
# Trident is the Internet Explorer browser engine
|
# Trident is the Internet Explorer browser engine
|
||||||
- userAgent.contains("Trident")
|
- userAgent.contains("Trident")
|
||||||
# Opera is a fork of chrome now
|
# Opera is a fork of chrome now
|
||||||
- userAgent.contains("Presto")
|
- userAgent.contains("Presto")
|
||||||
# Windows CE is discontinued
|
# Windows CE is discontinued
|
||||||
- userAgent.contains("Windows CE")
|
- userAgent.contains("Windows CE")
|
||||||
# Windows 95 is discontinued
|
# Windows 95 is discontinued
|
||||||
- userAgent.contains("Windows 95")
|
- userAgent.contains("Windows 95")
|
||||||
# Windows 98 is discontinued
|
# Windows 98 is discontinued
|
||||||
- userAgent.contains("Windows 98")
|
- userAgent.contains("Windows 98")
|
||||||
# Windows 9.x is discontinued
|
# Windows 9.x is discontinued
|
||||||
- userAgent.contains("Win 9x")
|
- userAgent.contains("Win 9x")
|
||||||
# Amazon does not have an Alexa Toolbar.
|
# Amazon does not have an Alexa Toolbar.
|
||||||
- userAgent.contains("Alexa Toolbar")
|
- userAgent.contains("Alexa Toolbar")
|
||||||
- name: challenge-aggressive-brazilian-scrapers
|
# This is not released, even Windows 11 calls itself Windows 10
|
||||||
action: CHALLENGE
|
- userAgent.contains("Windows NT 11.0")
|
||||||
expression:
|
# iPods are not in common use
|
||||||
any:
|
- userAgent.contains("iPod")
|
||||||
# This is not released, even Windows 11 calls itself Windows 10
|
|
||||||
- userAgent.contains("Windows NT 11.0")
|
|
||||||
# iPods are not in common use
|
|
||||||
- userAgent.contains("iPod")
|
|
||||||
|
|||||||
11
data/bots/ai-catchall.yaml
Normal file
11
data/bots/ai-catchall.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Extensive list of AI-affiliated agents based on https://github.com/ai-robots-txt/ai.robots.txt
|
||||||
|
# Add new/undocumented agents here. Where documentation exists, consider moving to dedicated policy files.
|
||||||
|
# Notes on various agents:
|
||||||
|
# - Amazonbot: Well documented, but they refuse to state which agent collects training data.
|
||||||
|
# - anthropic-ai/Claude-Web: Undocumented by Anthropic. Possibly deprecated or hallucinations?
|
||||||
|
# - Perplexity*: Well documented, but they refuse to state which agent collects training data.
|
||||||
|
# Warning: May contain user agents that _must_ be blocked in robots.txt, or the opt-out will have no effect.
|
||||||
|
- name: "ai-catchall"
|
||||||
|
user_agent_regex: >-
|
||||||
|
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Brightbot 1.0|Bytespider|CCBot|Claude-Web|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|GoogleOther|GoogleOther-Image|GoogleOther-Video|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|NovaAct|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|QualifiedBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YouBot
|
||||||
|
action: DENY
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
# Warning: Contains user agents that _must_ be blocked in robots.txt, or the opt-out will have no effect.
|
||||||
|
# Note: Blocks human-directed/non-training user agents
|
||||||
- name: "ai-robots-txt"
|
- name: "ai-robots-txt"
|
||||||
user_agent_regex: >-
|
user_agent_regex: >-
|
||||||
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Applebot|Applebot-Extended|Brightbot 1.0|Bytespider|CCBot|ChatGPT-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|NovaAct|OAI-SearchBot|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|YouBot
|
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|Andibot|anthropic-ai|Applebot|Applebot-Extended|bedrockbot|Brightbot 1.0|Bytespider|CCBot|ChatGPT-User|Claude-SearchBot|Claude-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|MistralAI-User/1.0|NovaAct|OAI-SearchBot|omgili|omgilibot|Operator|PanguBot|Panscient|panscient.com|Perplexity-User|PerplexityBot|PetalBot|PhindBot|QualifiedBot|QuillBot|quillbot.com|SBIntuitionsBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YandexAdditional|YandexAdditionalBot|YouBot
|
||||||
action: DENY
|
action: DENY
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
- name: cloudflare-workers
|
- name: cloudflare-workers
|
||||||
headers_regex:
|
headers_regex:
|
||||||
CF-Worker: .*
|
CF-Worker: .*
|
||||||
action: DENY
|
action: WEIGH
|
||||||
|
weight:
|
||||||
|
adjust: 15
|
||||||
|
|||||||
8
data/clients/ai.yaml
Normal file
8
data/clients/ai.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# User agents that act on behalf of humans in AI tools, e.g. searching the web.
|
||||||
|
# Each entry should have a positive/ALLOW entry created as well, with further documentation.
|
||||||
|
# Exceptions:
|
||||||
|
# - Claude-User: No published IP allowlist
|
||||||
|
- name: "ai-clients"
|
||||||
|
user_agent_regex: >-
|
||||||
|
ChatGPT-User|Claude-User|MistralAI-User
|
||||||
|
action: DENY
|
||||||
10
data/clients/mistral-mistralai-user.yaml
Normal file
10
data/clients/mistral-mistralai-user.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Acts on behalf of user requests
|
||||||
|
# https://docs.mistral.ai/robots/
|
||||||
|
- name: mistral-mistralai-user
|
||||||
|
user_agent_regex: MistralAI-User/.+; \+https\://docs\.mistral\.ai/robots
|
||||||
|
action: ALLOW
|
||||||
|
# https://mistral.ai/mistralai-user-ips.json
|
||||||
|
remote_addresses: [
|
||||||
|
"20.240.160.161/32",
|
||||||
|
"20.240.160.1/32",
|
||||||
|
]
|
||||||
93
data/clients/openai-chatgpt-user.yaml
Normal file
93
data/clients/openai-chatgpt-user.yaml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Acts on behalf of user requests
|
||||||
|
# https://platform.openai.com/docs/bots/overview-of-openai-crawlers
|
||||||
|
- name: openai-chatgpt-user
|
||||||
|
user_agent_regex: ChatGPT-User/.+; \+https\://openai\.com/bot
|
||||||
|
action: ALLOW
|
||||||
|
# https://openai.com/chatgpt-user.json
|
||||||
|
# curl 'https://openai.com/chatgpt-user.json' | jq '.prefixes.[].ipv4Prefix' | sed 's/$/,/'
|
||||||
|
remote_addresses: [
|
||||||
|
"13.65.138.112/28",
|
||||||
|
"23.98.179.16/28",
|
||||||
|
"13.65.138.96/28",
|
||||||
|
"172.183.222.128/28",
|
||||||
|
"20.102.212.144/28",
|
||||||
|
"40.116.73.208/28",
|
||||||
|
"172.183.143.224/28",
|
||||||
|
"52.190.190.16/28",
|
||||||
|
"13.83.237.176/28",
|
||||||
|
"51.8.155.64/28",
|
||||||
|
"74.249.86.176/28",
|
||||||
|
"51.8.155.48/28",
|
||||||
|
"20.55.229.144/28",
|
||||||
|
"135.237.131.208/28",
|
||||||
|
"135.237.133.48/28",
|
||||||
|
"51.8.155.112/28",
|
||||||
|
"135.237.133.112/28",
|
||||||
|
"52.159.249.96/28",
|
||||||
|
"52.190.137.16/28",
|
||||||
|
"52.255.111.112/28",
|
||||||
|
"40.84.181.32/28",
|
||||||
|
"172.178.141.112/28",
|
||||||
|
"52.190.142.64/28",
|
||||||
|
"172.178.140.144/28",
|
||||||
|
"52.190.137.144/28",
|
||||||
|
"172.178.141.128/28",
|
||||||
|
"57.154.187.32/28",
|
||||||
|
"4.196.118.112/28",
|
||||||
|
"20.193.50.32/28",
|
||||||
|
"20.215.188.192/28",
|
||||||
|
"20.215.214.16/28",
|
||||||
|
"4.197.22.112/28",
|
||||||
|
"4.197.115.112/28",
|
||||||
|
"172.213.21.16/28",
|
||||||
|
"172.213.11.144/28",
|
||||||
|
"172.213.12.112/28",
|
||||||
|
"172.213.21.144/28",
|
||||||
|
"20.90.7.144/28",
|
||||||
|
"57.154.175.0/28",
|
||||||
|
"57.154.174.112/28",
|
||||||
|
"52.236.94.144/28",
|
||||||
|
"137.135.191.176/28",
|
||||||
|
"23.98.186.192/28",
|
||||||
|
"23.98.186.96/28",
|
||||||
|
"23.98.186.176/28",
|
||||||
|
"23.98.186.64/28",
|
||||||
|
"68.221.67.192/28",
|
||||||
|
"68.221.67.160/28",
|
||||||
|
"13.83.167.128/28",
|
||||||
|
"20.228.106.176/28",
|
||||||
|
"52.159.227.32/28",
|
||||||
|
"68.220.57.64/28",
|
||||||
|
"172.213.21.112/28",
|
||||||
|
"68.221.67.224/28",
|
||||||
|
"68.221.75.16/28",
|
||||||
|
"20.97.189.96/28",
|
||||||
|
"52.252.113.240/28",
|
||||||
|
"52.230.163.32/28",
|
||||||
|
"172.212.159.64/28",
|
||||||
|
"52.255.111.80/28",
|
||||||
|
"52.255.111.0/28",
|
||||||
|
"4.151.241.240/28",
|
||||||
|
"52.255.111.32/28",
|
||||||
|
"52.255.111.48/28",
|
||||||
|
"52.255.111.16/28",
|
||||||
|
"52.230.164.176/28",
|
||||||
|
"52.176.139.176/28",
|
||||||
|
"52.173.234.16/28",
|
||||||
|
"4.151.71.176/28",
|
||||||
|
"4.151.119.48/28",
|
||||||
|
"52.255.109.112/28",
|
||||||
|
"52.255.109.80/28",
|
||||||
|
"20.161.75.208/28",
|
||||||
|
"68.154.28.96/28",
|
||||||
|
"52.255.109.128/28",
|
||||||
|
"52.225.75.208/28",
|
||||||
|
"52.190.139.48/28",
|
||||||
|
"68.221.67.240/28",
|
||||||
|
"52.156.77.144/28",
|
||||||
|
"52.148.129.32/28",
|
||||||
|
"40.84.221.208/28",
|
||||||
|
"104.210.139.224/28",
|
||||||
|
"40.84.221.224/28",
|
||||||
|
"104.210.139.192/28",
|
||||||
|
]
|
||||||
2
data/clients/small-internet-browsers/_permissive.yaml
Normal file
2
data/clients/small-internet-browsers/_permissive.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- import: (data)/clients/small-internet-browsers/netsurf.yaml
|
||||||
|
- import: (data)/clients/small-internet-browsers/palemoon.yaml
|
||||||
5
data/clients/small-internet-browsers/netsurf.yaml
Normal file
5
data/clients/small-internet-browsers/netsurf.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
- name: "reduce-weight-netsurf"
|
||||||
|
user_agent_regex: "NetSurf"
|
||||||
|
action: WEIGH
|
||||||
|
weight:
|
||||||
|
adjust: -5
|
||||||
5
data/clients/small-internet-browsers/palemoon.yaml
Normal file
5
data/clients/small-internet-browsers/palemoon.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
- name: "reduce-weight-palemoon"
|
||||||
|
user_agent_regex: "PaleMoon"
|
||||||
|
action: WEIGH
|
||||||
|
weight:
|
||||||
|
adjust: -5
|
||||||
6
data/clients/x-firefox-ai.yaml
Normal file
6
data/clients/x-firefox-ai.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# https://connect.mozilla.org/t5/firefox-labs/try-out-link-previews-in-firefox-labs-138-and-share-your/td-p/92012
|
||||||
|
- name: x-firefox-ai
|
||||||
|
action: WEIGH
|
||||||
|
expression: '"X-Firefox-Ai" in headers'
|
||||||
|
weight:
|
||||||
|
adjust: 5
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
- name: ipv4-rfc-1918
|
- name: ipv4-rfc-1918
|
||||||
action: ALLOW
|
action: ALLOW
|
||||||
remote_addresses:
|
remote_addresses:
|
||||||
- 10.0.0.0/8
|
- 10.0.0.0/8
|
||||||
- 172.16.0.0/12
|
- 172.16.0.0/12
|
||||||
- 192.168.0.0/16
|
- 192.168.0.0/16
|
||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
- name: ipv6-ula
|
- name: ipv6-ula
|
||||||
action: ALLOW
|
action: ALLOW
|
||||||
remote_addresses:
|
remote_addresses:
|
||||||
- fc00::/7
|
- fc00::/7
|
||||||
- name: ipv6-link-local
|
- name: ipv6-link-local
|
||||||
action: ALLOW
|
action: ALLOW
|
||||||
remote_addresses:
|
remote_addresses:
|
||||||
- fe80::/10
|
- fe80::/10
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
- import: (data)/crawlers/googlebot.yaml
|
- import: (data)/crawlers/googlebot.yaml
|
||||||
|
- import: (data)/crawlers/applebot.yaml
|
||||||
- import: (data)/crawlers/bingbot.yaml
|
- import: (data)/crawlers/bingbot.yaml
|
||||||
- import: (data)/crawlers/duckduckbot.yaml
|
- import: (data)/crawlers/duckduckbot.yaml
|
||||||
- import: (data)/crawlers/qwantbot.yaml
|
- import: (data)/crawlers/qwantbot.yaml
|
||||||
|
|||||||
8
data/crawlers/ai-search.yaml
Normal file
8
data/crawlers/ai-search.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# User agents that index exclusively for search in for AI systems.
|
||||||
|
# Each entry should have a positive/ALLOW entry created as well, with further documentation.
|
||||||
|
# Exceptions:
|
||||||
|
# - Claude-SearchBot: No published IP allowlist
|
||||||
|
- name: "ai-crawlers-search"
|
||||||
|
user_agent_regex: >-
|
||||||
|
OAI-SearchBot|Claude-SearchBot
|
||||||
|
action: DENY
|
||||||
8
data/crawlers/ai-training.yaml
Normal file
8
data/crawlers/ai-training.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# User agents that crawl for training AI/LLM systems
|
||||||
|
# Each entry should have a positive/ALLOW entry created as well, with further documentation.
|
||||||
|
# Exceptions:
|
||||||
|
# - ClaudeBot: No published IP allowlist
|
||||||
|
- name: "ai-crawlers-training"
|
||||||
|
user_agent_regex: >-
|
||||||
|
GPTBot|ClaudeBot
|
||||||
|
action: DENY
|
||||||
20
data/crawlers/applebot.yaml
Normal file
20
data/crawlers/applebot.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Indexing for search and Siri
|
||||||
|
# https://support.apple.com/en-us/119829
|
||||||
|
- name: applebot
|
||||||
|
user_agent_regex: Applebot
|
||||||
|
action: ALLOW
|
||||||
|
# https://search.developer.apple.com/applebot.json
|
||||||
|
remote_addresses: [
|
||||||
|
"17.241.208.160/27",
|
||||||
|
"17.241.193.160/27",
|
||||||
|
"17.241.200.160/27",
|
||||||
|
"17.22.237.0/24",
|
||||||
|
"17.22.245.0/24",
|
||||||
|
"17.22.253.0/24",
|
||||||
|
"17.241.75.0/24",
|
||||||
|
"17.241.219.0/24",
|
||||||
|
"17.241.227.0/24",
|
||||||
|
"17.246.15.0/24",
|
||||||
|
"17.246.19.0/24",
|
||||||
|
"17.246.23.0/24",
|
||||||
|
]
|
||||||
16
data/crawlers/openai-gptbot.yaml
Normal file
16
data/crawlers/openai-gptbot.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Collects AI training data
|
||||||
|
# https://platform.openai.com/docs/bots/overview-of-openai-crawlers
|
||||||
|
- name: openai-gptbot
|
||||||
|
user_agent_regex: GPTBot/1\.1; \+https\://openai\.com/gptbot
|
||||||
|
action: ALLOW
|
||||||
|
# https://openai.com/gptbot.json
|
||||||
|
remote_addresses: [
|
||||||
|
"52.230.152.0/24",
|
||||||
|
"20.171.206.0/24",
|
||||||
|
"20.171.207.0/24",
|
||||||
|
"4.227.36.0/25",
|
||||||
|
"20.125.66.80/28",
|
||||||
|
"172.182.204.0/24",
|
||||||
|
"172.182.214.0/24",
|
||||||
|
"172.182.215.0/24",
|
||||||
|
]
|
||||||
13
data/crawlers/openai-searchbot.yaml
Normal file
13
data/crawlers/openai-searchbot.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Indexing for search, does not collect training data
|
||||||
|
# https://platform.openai.com/docs/bots/overview-of-openai-crawlers
|
||||||
|
- name: openai-searchbot
|
||||||
|
user_agent_regex: OAI-SearchBot/1\.0; \+https\://openai\.com/searchbot
|
||||||
|
action: ALLOW
|
||||||
|
# https://openai.com/searchbot.json
|
||||||
|
remote_addresses: [
|
||||||
|
"20.42.10.176/28",
|
||||||
|
"172.203.190.128/28",
|
||||||
|
"104.210.140.128/28",
|
||||||
|
"51.8.102.0/24",
|
||||||
|
"135.234.64.0/24"
|
||||||
|
]
|
||||||
@@ -3,6 +3,6 @@ package data
|
|||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed botPolicies.yaml botPolicies.json all:apps all:bots all:clients all:common all:crawlers
|
//go:embed botPolicies.yaml botPolicies.json all:apps all:bots all:clients all:common all:crawlers all:meta
|
||||||
BotPolicies embed.FS
|
BotPolicies embed.FS
|
||||||
)
|
)
|
||||||
|
|||||||
5
data/meta/README.md
Normal file
5
data/meta/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# meta policies
|
||||||
|
|
||||||
|
Contains policies that exclusively reference policies in _multiple_ other data folders.
|
||||||
|
|
||||||
|
Akin to "stances" that the administrator can take, with reference to various topics, such as AI/LLM systems.
|
||||||
6
data/meta/ai-block-aggressive.yaml
Normal file
6
data/meta/ai-block-aggressive.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Blocks all AI/LLM associated user agents, regardless of purpose or human agency
|
||||||
|
# Warning: To completely block some AI/LLM training, such as with Google, you _must_ place flags in robots.txt.
|
||||||
|
- import: (data)/bots/ai-catchall.yaml
|
||||||
|
- import: (data)/clients/ai.yaml
|
||||||
|
- import: (data)/crawlers/ai-search.yaml
|
||||||
|
- import: (data)/crawlers/ai-training.yaml
|
||||||
7
data/meta/ai-block-moderate.yaml
Normal file
7
data/meta/ai-block-moderate.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Blocks all AI/LLM bots used for training or unknown/undocumented purposes.
|
||||||
|
# Permits user agents with explicitly documented non-training use, and published IP allowlists.
|
||||||
|
- import: (data)/bots/ai-catchall.yaml
|
||||||
|
- import: (data)/crawlers/ai-training.yaml
|
||||||
|
- import: (data)/crawlers/openai-searchbot.yaml
|
||||||
|
- import: (data)/clients/openai-chatgpt-user.yaml
|
||||||
|
- import: (data)/clients/mistral-mistralai-user.yaml
|
||||||
6
data/meta/ai-block-permissive.yaml
Normal file
6
data/meta/ai-block-permissive.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Permits all well documented AI/LLM user agents with published IP allowlists.
|
||||||
|
- import: (data)/bots/ai-catchall.yaml
|
||||||
|
- import: (data)/crawlers/openai-searchbot.yaml
|
||||||
|
- import: (data)/crawlers/openai-gptbot.yaml
|
||||||
|
- import: (data)/clients/openai-chatgpt-user.yaml
|
||||||
|
- import: (data)/clients/mistral-mistralai-user.yaml
|
||||||
27
docker-bake.hcl
Normal file
27
docker-bake.hcl
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
variable "ALPINE_VERSION" { default = "3.22" }
|
||||||
|
variable "GITHUB_SHA" { default = "devel" }
|
||||||
|
|
||||||
|
group "default" {
|
||||||
|
targets = [
|
||||||
|
"anubis",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
target "anubis" {
|
||||||
|
args = {
|
||||||
|
ALPINE_VERSION = "3.22"
|
||||||
|
}
|
||||||
|
context = "."
|
||||||
|
dockerfile = "./Dockerfile"
|
||||||
|
platforms = [
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm64",
|
||||||
|
"linux/arm/v7",
|
||||||
|
"linux/ppc64le",
|
||||||
|
"linux/riscv64",
|
||||||
|
]
|
||||||
|
pull = true
|
||||||
|
tags = [
|
||||||
|
"ghcr.io/techarohq/anubis:${GITHUB_SHA}"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -11,13 +11,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Remove the unused `/test-error` endpoint and update the testing endpoint `/make-challenge` to only be enabled in
|
||||||
|
development
|
||||||
|
- Add `--xff-strip-private` flag/envvar to toggle skipping X-Forwarded-For private addresses or not
|
||||||
|
- Requests can have their weight be adjusted, if a request weighs zero or less than it is allowed through
|
||||||
|
- Refactor challenge presentation logic to use a challenge registry
|
||||||
|
- Allow challenge implementations to register HTTP routes
|
||||||
|
- Implement a no-JS challenge method: [`metarefresh`](./admin/configuration/challenges/metarefresh.mdx) ([#95](https://github.com/TecharoHQ/anubis/issues/95))
|
||||||
|
- Bump AI-robots.txt to version 1.34
|
||||||
|
- Make progress bar styling more compatible (UXP, etc)
|
||||||
|
|
||||||
|
## v1.19.1: Jenomis cen Lexentale - Echo 1
|
||||||
|
|
||||||
|
- Return `data/bots/ai-robots-txt.yaml` to avoid breaking configs [#599](https://github.com/TecharoHQ/anubis/issues/599)
|
||||||
|
|
||||||
|
## v1.19.0: Jenomis cen Lexentale
|
||||||
|
|
||||||
|
Mostly a bunch of small features, no big ticket things this time.
|
||||||
|
|
||||||
|
- Record if challenges were issued via the API or via embedded JSON in the challenge page HTML ([#531](https://github.com/TecharoHQ/anubis/issues/531))
|
||||||
|
- Ensure that clients that are shown a challenge support storing cookies
|
||||||
|
- Imprint the version number into challenge pages
|
||||||
|
- Encode challenge pages with gzip level 1
|
||||||
|
- Add PowerPC 64 bit little-endian builds (`GOARCH=ppc64le`)
|
||||||
- Add `check-spelling` for spell checking
|
- Add `check-spelling` for spell checking
|
||||||
- Add `--target-insecure-skip-verify` flag/envvar to allow Anubis to hit a self-signed HTTPS backend
|
- Add `--target-insecure-skip-verify` flag/envvar to allow Anubis to hit a self-signed HTTPS backend
|
||||||
- Minor adjustments to FreeBSD rc.d script to allow for more flexible configuration.
|
- Minor adjustments to FreeBSD rc.d script to allow for more flexible configuration.
|
||||||
- Added Podman and Docker support for running Playwright tests
|
- Added Podman and Docker support for running Playwright tests
|
||||||
|
- Add a default rule to throw challenges when a request with the `X-Firefox-Ai` header is set
|
||||||
- Updated the nonce value in the challenge JWT cookie to be a string instead of a number
|
- Updated the nonce value in the challenge JWT cookie to be a string instead of a number
|
||||||
- Rename cookies in response to user feedback
|
- Rename cookies in response to user feedback
|
||||||
- Ensure cookie renaming is consistent across configuration options
|
- Ensure cookie renaming is consistent across configuration options
|
||||||
|
- Add Bookstack app in data
|
||||||
|
- Truncate everything but the first five characters of Accept-Language headers when making challenges
|
||||||
|
- Ensure client JavaScript is served with Content-Type text/javascript.
|
||||||
|
- Add `--target-host` flag/envvar to allow changing the value of the Host header in requests forwarded to the target service
|
||||||
|
- Bump AI-robots.txt to version 1.31
|
||||||
|
- Add `RuntimeDirectory` to systemd unit settings so native packages can listen over unix sockets
|
||||||
|
- Added SearXNG instance tracker whitelist policy
|
||||||
|
- Added Qualys SSL Labs whitelist policy
|
||||||
|
- Fixed cookie deletion logic ([#520](https://github.com/TecharoHQ/anubis/issues/520), [#522](https://github.com/TecharoHQ/anubis/pull/522))
|
||||||
|
- Add `--target-sni` flag/envvar to allow changing the value of the TLS handshake hostname in requests forwarded to the target service
|
||||||
|
- Fixed CEL expression matching validator to now properly error out when it receives empty expressions
|
||||||
|
- Added OpenRC init.d script
|
||||||
|
- Added `--version` flag
|
||||||
|
- Added `anubis_proxied_requests_total` metric to count proxied requests
|
||||||
|
- Add `Applebot` as "good" web crawler
|
||||||
|
- Reorganize AI/LLM crawler blocking into three separate stances, maintaining existing status quo as default
|
||||||
|
- Split out AI/LLM user agent blocking policies, adding documentation for each
|
||||||
|
|
||||||
## v1.18.0: Varis zos Galvus
|
## v1.18.0: Varis zos Galvus
|
||||||
|
|
||||||
@@ -43,7 +84,7 @@ Or as complicated as:
|
|||||||
expression:
|
expression:
|
||||||
all:
|
all:
|
||||||
- >-
|
- >-
|
||||||
(
|
(
|
||||||
userAgent.startsWith("git/") ||
|
userAgent.startsWith("git/") ||
|
||||||
userAgent.contains("libgit") ||
|
userAgent.contains("libgit") ||
|
||||||
userAgent.startsWith("go-git") ||
|
userAgent.startsWith("go-git") ||
|
||||||
|
|||||||
8
docs/docs/admin/configuration/challenges/_category_.json
Normal file
8
docs/docs/admin/configuration/challenges/_category_.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"label": "Challenges",
|
||||||
|
"position": 10,
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"description": "The different challenge methods that Anubis supports."
|
||||||
|
}
|
||||||
|
}
|
||||||
19
docs/docs/admin/configuration/challenges/metarefresh.mdx
Normal file
19
docs/docs/admin/configuration/challenges/metarefresh.mdx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Meta Refresh (No JavaScript)
|
||||||
|
|
||||||
|
The `metarefresh` challenge sends a browser a much simpler challenge that makes it refresh the page after a set period of time. This enables clients to pass challenges without executing JavaScript.
|
||||||
|
|
||||||
|
To use it in your Anubis configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Generic catchall rule
|
||||||
|
- name: generic-browser
|
||||||
|
user_agent_regex: >-
|
||||||
|
Mozilla|Opera
|
||||||
|
action: CHALLENGE
|
||||||
|
challenge:
|
||||||
|
difficulty: 1 # Number of seconds to wait before refreshing the page
|
||||||
|
report_as: 4 # Unused by this challenge method
|
||||||
|
algorithm: metarefresh # Specify a non-JS challenge method
|
||||||
|
```
|
||||||
|
|
||||||
|
This is not enabled by default while this method is tested and its false positive rate is ascertained. Many modern scrapers use headless Google Chrome, so this will have a much higher false positive rate.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Proof of Work (JavaScript)
|
||||||
|
|
||||||
|
When Anubis is configured to use the `fast` or `slow` challenge methods, clients will be sent a small [proof of work](https://en.wikipedia.org/wiki/Proof_of_work) challenge. In order to get a token used to access the upstream resource, clients must calculate a complicated math puzzle with JavaScript.
|
||||||
|
|
||||||
|
A `fast` challenge uses a heavily optimized multithreaded implementation and a `slow` challenge uses a simplistic single-threaded implementation. The `slow` method is kept around for legacy compatibility.
|
||||||
@@ -143,7 +143,29 @@ Anubis would return a challenge because all of those conditions are true.
|
|||||||
|
|
||||||
## Functions exposed to Anubis expressions
|
## Functions exposed to Anubis expressions
|
||||||
|
|
||||||
There are currently no functions from the Anubis runtime exposed to expressions. This will change in the future.
|
Anubis expressions can be augmented with the following functions:
|
||||||
|
|
||||||
|
### `randInt`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function randInt(n: int): int;
|
||||||
|
```
|
||||||
|
|
||||||
|
randInt returns a randomly selected integer value in the range of `[0,n)`. This is a thin wrapper around [Go's math/rand#Intn](https://pkg.go.dev/math/rand#Intn). Be careful with this as it may cause inconsistent behavior for genuine users.
|
||||||
|
|
||||||
|
This is best applied when doing explicit block rules, eg:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Denies LightPanda about 75% of the time on average
|
||||||
|
- name: deny-lightpanda-sometimes
|
||||||
|
action: DENY
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- userAgent.matches("LightPanda")
|
||||||
|
- randInt(16) >= 4
|
||||||
|
```
|
||||||
|
|
||||||
|
It seems counter-intuitive to allow known bad clients through sometimes, but this allows you to confuse attackers by making Anubis' behavior random. Adjust the thresholds and numbers as facts and circumstances demand.
|
||||||
|
|
||||||
## Life advice
|
## Life advice
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ EG:
|
|||||||
{
|
{
|
||||||
"bots": [
|
"bots": [
|
||||||
{
|
{
|
||||||
"import": "(data)/bots/ai-robots-txt.yaml"
|
"import": "(data)/bots/ai-catchall.yaml"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"import": "(data)/bots/cloudflare-workers.yaml"
|
"import": "(data)/bots/cloudflare-workers.yaml"
|
||||||
@@ -29,8 +29,8 @@ EG:
|
|||||||
```yaml
|
```yaml
|
||||||
bots:
|
bots:
|
||||||
# Pathological bots to deny
|
# Pathological bots to deny
|
||||||
- # This correlates to data/bots/ai-robots-txt.yaml in the source tree
|
- # This correlates to data/bots/ai-catchall.yaml in the source tree
|
||||||
import: (data)/bots/ai-robots-txt.yaml
|
import: (data)/bots/ai-catchall.yaml
|
||||||
- import: (data)/bots/cloudflare-workers.yaml
|
- import: (data)/bots/cloudflare-workers.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ Of note, a bot rule can either have inline bot configuration or import a bot con
|
|||||||
{
|
{
|
||||||
"bots": [
|
"bots": [
|
||||||
{
|
{
|
||||||
"import": "(data)/bots/ai-robots-txt.yaml",
|
"import": "(data)/bots/ai-catchall.yaml",
|
||||||
"name": "generic-browser",
|
"name": "generic-browser",
|
||||||
"user_agent_regex": "Mozilla|Opera\n",
|
"user_agent_regex": "Mozilla|Opera\n",
|
||||||
"action": "CHALLENGE"
|
"action": "CHALLENGE"
|
||||||
@@ -60,7 +60,7 @@ Of note, a bot rule can either have inline bot configuration or import a bot con
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
bots:
|
bots:
|
||||||
- import: (data)/bots/ai-robots-txt.yaml
|
- import: (data)/bots/ai-catchall.yaml
|
||||||
name: generic-browser
|
name: generic-browser
|
||||||
user_agent_regex: >
|
user_agent_regex: >
|
||||||
Mozilla|Opera
|
Mozilla|Opera
|
||||||
@@ -167,7 +167,7 @@ static
|
|||||||
├── botPolicies.json
|
├── botPolicies.json
|
||||||
├── botPolicies.yaml
|
├── botPolicies.yaml
|
||||||
├── bots
|
├── bots
|
||||||
│ ├── ai-robots-txt.yaml
|
│ ├── ai-catchall.yaml
|
||||||
│ ├── cloudflare-workers.yaml
|
│ ├── cloudflare-workers.yaml
|
||||||
│ ├── headless-browsers.yaml
|
│ ├── headless-browsers.yaml
|
||||||
│ └── us-ai-scraper.yaml
|
│ └── us-ai-scraper.yaml
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ Anubis can act in one of two modes:
|
|||||||
1. Reverse proxy (the default): Anubis sits in the middle of all traffic and then will reverse proxy it to its destination. This is the moral equivalent of a middleware in your favorite web framework.
|
1. Reverse proxy (the default): Anubis sits in the middle of all traffic and then will reverse proxy it to its destination. This is the moral equivalent of a middleware in your favorite web framework.
|
||||||
2. Subrequest authentication mode: Anubis listens for requests and if they don't pass muster then they are forwarded to Anubis for challenge processing. This is the equivalent of Anubis being a sidecar service.
|
2. Subrequest authentication mode: Anubis listens for requests and if they don't pass muster then they are forwarded to Anubis for challenge processing. This is the equivalent of Anubis being a sidecar service.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
Subrequest authentication requires changing the default policy because nginx interprets the default `DENY` status code `200` as successful authentication and allows the request.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
status_codes:
|
||||||
|
CHALLENGE: 200
|
||||||
|
DENY: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
[See policy definitions](../policies.mdx).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
## Nginx
|
## Nginx
|
||||||
|
|
||||||
Anubis can perform [subrequest authentication](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/) with the `auth_request` module in Nginx. In order to set this up, keep the following things in mind:
|
Anubis can perform [subrequest authentication](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/) with the `auth_request` module in Nginx. In order to set this up, keep the following things in mind:
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ Assuming you are protecting `anubistest.techaro.lol`, you need the following ser
|
|||||||
# throw an "admin misconfiguration" error.
|
# throw an "admin misconfiguration" error.
|
||||||
RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR}
|
RequestHeader set "X-Real-Ip" expr=%{REMOTE_ADDR}
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
RequestHeader set X-Forwarded-Proto "https"
|
||||||
|
RequestHeader set "X-Http-Version" "%{SERVER_PROTOCOL}s"
|
||||||
|
|
||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
|
|
||||||
@@ -119,6 +120,14 @@ Make sure to add a separate configuration file for the listener on port 3001:
|
|||||||
```text
|
```text
|
||||||
# /etc/httpd/conf.d/listener-3001.conf
|
# /etc/httpd/conf.d/listener-3001.conf
|
||||||
|
|
||||||
|
Listen [::1]:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you are running an IPv4-only system, use the following configuration instead:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# /etc/httpd/conf.d/listener-3001.conf
|
||||||
|
|
||||||
Listen 127.0.0.1:3001
|
Listen 127.0.0.1:3001
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ server {
|
|||||||
location / {
|
location / {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Http-Version $server_protocol;
|
||||||
proxy_pass http://anubis;
|
proxy_pass http://anubis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ id: traefik
|
|||||||
title: Traefik
|
title: Traefik
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
|
|
||||||
This only talks about integration through Compose,
|
This only talks about integration through Compose,
|
||||||
but it also applies to docker cli options.
|
but it also applies to docker cli options.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
8
docs/docs/admin/frameworks/_category_.json
Normal file
8
docs/docs/admin/frameworks/_category_.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"label": "Frameworks",
|
||||||
|
"position": 30,
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"description": "Information about getting specific frameworks or tools working with Anubis."
|
||||||
|
}
|
||||||
|
}
|
||||||
45
docs/docs/admin/frameworks/htmx.mdx
Normal file
45
docs/docs/admin/frameworks/htmx.mdx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# HTMX
|
||||||
|
|
||||||
|
import Tabs from "@theme/Tabs";
|
||||||
|
import TabItem from "@theme/TabItem";
|
||||||
|
|
||||||
|
[HTMX](https://htmx.org) is a framework that enables you to write applications using hypertext as the engine of application state. This enables you to simplify you server side code by having it return HTML instead of JSON. This can interfere with Anubis because Anubis challenge pages also return HTML.
|
||||||
|
|
||||||
|
To work around this, you can make a custom [expression](../configuration/expressions.mdx) rule that allows HTMX requests if the user has passed a challenge in the past:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="json" label="JSON">
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "allow-htmx-iff-already-passed-challenge",
|
||||||
|
"action": "ALLOW",
|
||||||
|
"expression": {
|
||||||
|
"all": [
|
||||||
|
"\"Cookie\" in headers",
|
||||||
|
"headers[\"Cookie\"].contains(\"anubis-auth\")",
|
||||||
|
"\"Hx-Request\" in headers",
|
||||||
|
"headers[\"Hx-Request\"] == \"true\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="yaml" label="YAML" default>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: allow-htmx-iff-already-passed-challenge
|
||||||
|
action: ALLOW
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- '"Cookie" in headers'
|
||||||
|
- 'headers["Cookie"].contains("anubis-auth")'
|
||||||
|
- '"Hx-Request" in headers'
|
||||||
|
- 'headers["Hx-Request"] == "true"'
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
This will reduce some security because it does not assert the validity of the Anubis auth cookie, however in trade it improves the experience for existing users.
|
||||||
39
docs/docs/admin/frameworks/wordpress.mdx
Normal file
39
docs/docs/admin/frameworks/wordpress.mdx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Wordpress
|
||||||
|
|
||||||
|
Wordpress is the most popular blog engine on the planet.
|
||||||
|
|
||||||
|
## Using a multi-site setup with Anubis
|
||||||
|
|
||||||
|
If you have a multi-site setup where traffic goes through Anubis like this:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
title: Apache as tls terminator and HTTP router
|
||||||
|
---
|
||||||
|
|
||||||
|
flowchart LR
|
||||||
|
T(User Traffic)
|
||||||
|
subgraph Apache 2
|
||||||
|
TCP(TCP 80/443)
|
||||||
|
US(TCP 3001)
|
||||||
|
end
|
||||||
|
|
||||||
|
An(Anubis)
|
||||||
|
B(Backend)
|
||||||
|
|
||||||
|
T --> |TLS termination| TCP
|
||||||
|
TCP --> |Traffic filtering| An
|
||||||
|
An --> |Happy traffic| US
|
||||||
|
US --> |whatever you're doing| B
|
||||||
|
```
|
||||||
|
|
||||||
|
Wordpress may not realize that the underlying connection is being done over HTTPS. This could lead to a redirect loop in the `/wp-admin/` routes. In order to fix this, add the following to your `wp-config.php` file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
||||||
|
$_SERVER['HTTPS'] = 'on';
|
||||||
|
$_SERVER['SERVER_PORT'] = 443;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will make Wordpress think that your connection is over HTTPS instead of plain HTTP.
|
||||||
@@ -49,29 +49,30 @@ For more detailed information on installing Anubis with native packages, please
|
|||||||
|
|
||||||
Anubis uses these environment variables for configuration:
|
Anubis uses these environment variables for configuration:
|
||||||
|
|
||||||
| Environment Variable | Default value | Explanation |
|
| Environment Variable | Default value | Explanation |
|
||||||
| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints. For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
|
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints. For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
|
||||||
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
||||||
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
||||||
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information. |
|
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.<br/><br/>Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. |
|
||||||
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
||||||
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||||
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||||
| `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. |
|
| `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. |
|
||||||
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
|
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
|
||||||
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
||||||
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
| `METRICS_BIND_NETWORK` | `tcp` | The 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_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_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. |
|
||||||
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. |
|
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. |
|
||||||
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
|
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
|
||||||
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain. |
|
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.<br/><br/>Note that if you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here. |
|
||||||
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
|
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
|
||||||
| `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. |
|
| `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. |
|
||||||
| `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. |
|
| `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. |
|
||||||
| `USE_REMOTE_ADDRESS` | unset | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead. |
|
| `USE_REMOTE_ADDRESS` | unset | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead. |
|
||||||
| `WEBMASTER_EMAIL` | unset | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators. |
|
| `WEBMASTER_EMAIL` | unset | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators. |
|
||||||
|
| `XFF_STRIP_PRIVATE` | `true` | If set, strip private addresses from `X-Forwarded-For` headers. To unset this, you must set `XFF_STRIP_PRIVATE=false` or `--xff-strip-private=false`. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Advanced configuration settings</summary>
|
<summary>Advanced configuration settings</summary>
|
||||||
@@ -84,6 +85,8 @@ If you don't know or understand what these settings mean, ignore them. These are
|
|||||||
|
|
||||||
| Environment Variable | Default value | Explanation |
|
| Environment Variable | Default value | Explanation |
|
||||||
| :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
|
||||||
|
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
|
||||||
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
|
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ sudo install -D ./run/anubis@.service /etc/systemd/system
|
|||||||
Install the default configuration file to your system:
|
Install the default configuration file to your system:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
sudo install -D ./run/default.env /etc/anubis
|
sudo install -D ./run/default.env /etc/anubis/default.env
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
@@ -77,6 +77,13 @@ Install Anubis with `rpm`:
|
|||||||
sudo rpm -ivh ./anubis-$VERSION.$ARCH.rpm
|
sudo rpm -ivh ./anubis-$VERSION.$ARCH.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="distro" label="Package managers">
|
||||||
|
|
||||||
|
Some Linux distributions offer Anubis [as a native package](https://repology.org/project/anubis-anti-crawler/versions). If you want to install Anubis from your distribution's package manager, consult any upstream documentation for how to install the package. It will either be named `anubis`, `www-apps/anubis` or `www/anubis`.
|
||||||
|
|
||||||
|
If you use a systemd-flavoured distribution, then follow the setup instructions for Debian or Red Hat Linux.
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -244,3 +244,39 @@ In case your service needs it for risk calculation reasons, Anubis exposes infor
|
|||||||
| `X-Anubis-Status` | The status and how strict Anubis was in its checks | `PASS` |
|
| `X-Anubis-Status` | The status and how strict Anubis was in its checks | `PASS` |
|
||||||
|
|
||||||
Policy rules are matched using [Go's standard library regular expressions package](https://pkg.go.dev/regexp). You can mess around with the syntax at [regex101.com](https://regex101.com), make sure to select the Golang option.
|
Policy rules are matched using [Go's standard library regular expressions package](https://pkg.go.dev/regexp). You can mess around with the syntax at [regex101.com](https://regex101.com), make sure to select the Golang option.
|
||||||
|
|
||||||
|
## Request Weight
|
||||||
|
|
||||||
|
Anubis rules can also add or remove "weight" from requests, allowing administrators to configure custom levels of suspicion. For example, if your application uses session tokens named `i_love_gitea`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: gitea-session-token
|
||||||
|
action: WEIGH
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- '"Cookie" in headers'
|
||||||
|
- headers["Cookie"].contains("i_love_gitea=")
|
||||||
|
# Remove 5 weight points
|
||||||
|
weight:
|
||||||
|
adjust: -5
|
||||||
|
```
|
||||||
|
|
||||||
|
This would remove five weight points from the request, making Anubis present the [Meta Refresh challenge](./configuration/challenges/metarefresh.mdx).
|
||||||
|
|
||||||
|
### Weight Thresholds
|
||||||
|
|
||||||
|
Weight thresholds and challenge associations will be configurable with CEL expressions in the configuration file in an upcoming patch, for now here's how Anubis configures the weight thresholds:
|
||||||
|
|
||||||
|
| Weight Expression | Action |
|
||||||
|
| -----------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `weight < 0` (weight is less than 0) | Allow the request through. |
|
||||||
|
| `weight < 10` (weight is less than 10) | Challenge the client with the [Meta Refresh challenge](./configuration/challenges/metarefresh.mdx) at the default difficulty level. |
|
||||||
|
| `weight >= 10` (weight is greater than or equal to 10) | Challenge the client with the [Proof of Work challenge](./configuration/challenges/proof-of-work.mdx) at the default difficulty level. |
|
||||||
|
|
||||||
|
### Advice
|
||||||
|
|
||||||
|
Weight is still very new and needs work. This is an experimental feature and should be treated as such. Here's some advice to help you better tune requests:
|
||||||
|
|
||||||
|
- The default weight for browser-like clients is 10. This triggers an aggressive challenge.
|
||||||
|
- Remove and add weight in multiples of five.
|
||||||
|
- Be careful with how you configure weight.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ work valid?"}
|
|||||||
|
|
||||||
## 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:
|
When a client passes a challenge, Anubis sets an HTTP cookie named `"techaro.lol-anubis-auth"` containing a signed [JWT](https://jwt.io/) (JSON Web Token). This JWT contains the following claims:
|
||||||
|
|
||||||
- `challenge`: The challenge string derived from user request metadata
|
- `challenge`: The challenge string derived from user request metadata
|
||||||
- `nonce`: The nonce / iteration number used to generate the passing response
|
- `nonce`: The nonce / iteration number used to generate the passing response
|
||||||
|
|||||||
@@ -19,14 +19,48 @@ title: Anubis
|
|||||||
|
|
||||||
Anubis is brought to you by sponsors and donors like:
|
Anubis is brought to you by sponsors and donors like:
|
||||||
|
|
||||||
[](https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis)
|
### Diamond Tier
|
||||||
[](https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh)
|
|
||||||
[](https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis)
|
<a href="https://www.raptorcs.com/content/base/products.html">
|
||||||
[](https://weblate.org/?utm_campaign=github&utm_medium=referral&utm_content=anubis)
|
<img
|
||||||
|
src="/img/sponsors/raptor-computing-logo.webp"
|
||||||
|
alt="Raptor Computing Systems"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### Gold Tier
|
||||||
|
|
||||||
|
<a href="https://distrust.co?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img src="/img/sponsors/distrust-logo.webp" alt="Distrust" height="64" />
|
||||||
|
</a>
|
||||||
|
<a href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=anubis&utm_source=abgh">
|
||||||
|
<img
|
||||||
|
src="/img/sponsors/terminal-trove.webp"
|
||||||
|
alt="Terminal Trove"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://canine.tools?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img
|
||||||
|
src="/img/sponsors/caninetools-logo.webp"
|
||||||
|
alt="canine.tools"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://weblate.org/">
|
||||||
|
<img src="/img/sponsors/weblate-logo.webp" alt="Weblate" height="64" />
|
||||||
|
</a>
|
||||||
|
<a href="https://uberspace.de/">
|
||||||
|
<img src="/img/sponsors/uberspace-logo.webp" alt="Uberspace" height="64" />
|
||||||
|
</a>
|
||||||
|
<a href="https://wildbase.xyz/">
|
||||||
|
<img src="/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Anubis [weighs the soul of your connection](https://en.wikipedia.org/wiki/Weighing_of_souls) using a proof-of-work challenge in order to protect upstream resources from scraper bots.
|
Anubis is a Web AI Firewall Utility that [weighs the soul of your connection](https://en.wikipedia.org/wiki/Weighing_of_souls) using one or more challenges in order to protect upstream resources from scraper bots.
|
||||||
|
|
||||||
This program is designed to help protect the small internet from the endless storm of requests that flood in from AI companies. Anubis is as lightweight as possible to ensure that everyone can afford to protect the communities closest to them.
|
This program is designed to help protect the small internet from the endless storm of requests that flood in from AI companies. Anubis is as lightweight as possible to ensure that everyone can afford to protect the communities closest to them.
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,11 @@ Anubis uses [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_W
|
|||||||
2. Web Workers allow you to do multithreaded execution of JavaScript code. This lets Anubis run its checks in parallel across all your system cores so that the challenge can complete as fast as possible. In the last decade, most CPU advancements have come from making cores and code extremely parallel. Using Web Workers lets Anubis take advantage of your hardware as much as possible so that the challenge finishes as fast as possible.
|
2. Web Workers allow you to do multithreaded execution of JavaScript code. This lets Anubis run its checks in parallel across all your system cores so that the challenge can complete as fast as possible. In the last decade, most CPU advancements have come from making cores and code extremely parallel. Using Web Workers lets Anubis take advantage of your hardware as much as possible so that the challenge finishes as fast as possible.
|
||||||
|
|
||||||
If you use a browser extension such as [JShelter](https://jshelter.org/), you will need to [modify your JShelter configuration](./known-broken-extensions.md#jshelter) to allow Anubis' proof of work computation to complete.
|
If you use a browser extension such as [JShelter](https://jshelter.org/), you will need to [modify your JShelter configuration](./known-broken-extensions.md#jshelter) to allow Anubis' proof of work computation to complete.
|
||||||
|
|
||||||
|
## Does Anubis mine Bitcoin?
|
||||||
|
|
||||||
|
No. Anubis does not mine Bitcoin.
|
||||||
|
|
||||||
|
In order to mine bitcoin, you need to download a copy of the blockchain (so you have the state required to do mining) and also broadcast your mined blocks to the network should you reach a hash with the right number of leading zeroes. You also need to continuously read for newly broadcasted transactions so you can batch them into a block. This requires gigabytes of data to be transferred from the server to the client.
|
||||||
|
|
||||||
|
Anubis transfers two digit numbers of kilobytes from the server to the client (which you can independently verify with your browser's Developer Tools feature). This is orders of magnitude below what is required to mine Bitcoin.
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
|||||||
- https://bugzilla.proxmox.com
|
- https://bugzilla.proxmox.com
|
||||||
- https://hofstede.io/
|
- https://hofstede.io/
|
||||||
- https://www.indiemag.fr/
|
- https://www.indiemag.fr/
|
||||||
|
- https://reddit.nerdvpn.de/
|
||||||
|
- https://hosted.weblate.org/
|
||||||
|
- https://gitea.com/
|
||||||
|
- https://openwrt.org/
|
||||||
|
- https://minihoot.site
|
||||||
|
- https://catgirl.click/
|
||||||
|
- https://wiki.dolphin-emu.org/
|
||||||
- <details>
|
- <details>
|
||||||
<summary>FreeCAD</summary>
|
<summary>FreeCAD</summary>
|
||||||
- https://forum.freecad.org/
|
- https://forum.freecad.org/
|
||||||
@@ -57,3 +64,10 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
|||||||
<summary>The United Nations</summary>
|
<summary>The United Nations</summary>
|
||||||
- https://policytoolbox.iiep.unesco.org/
|
- https://policytoolbox.iiep.unesco.org/
|
||||||
</details>
|
</details>
|
||||||
|
- <details>
|
||||||
|
<summary>hebis (Alliance of Hessian Libraries)</summary>
|
||||||
|
- https://ubmr.hds.hebis.de/
|
||||||
|
- https://tufind.hds.hebis.de/
|
||||||
|
- https://karla.hds.hebis.de/
|
||||||
|
- and many more (see https://www.hebis.de/dienste/hebis-discovery-system/)
|
||||||
|
</details>
|
||||||
|
|||||||
72
docs/manifest/cfg/anubis/botPolicies.yaml
Normal file
72
docs/manifest/cfg/anubis/botPolicies.yaml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
## Anubis has the ability to let you import snippets of configuration into the main
|
||||||
|
## configuration file. This allows you to break up your config into smaller parts
|
||||||
|
## that get logically assembled into one big file.
|
||||||
|
##
|
||||||
|
## Of note, a bot rule can either have inline bot configuration or import a
|
||||||
|
## bot config snippet. You cannot do both in a single bot rule.
|
||||||
|
##
|
||||||
|
## Import paths can either be prefixed with (data) to import from the common/shared
|
||||||
|
## rules in the data folder in the Anubis source tree or will point to absolute/relative
|
||||||
|
## paths in your 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.
|
||||||
|
|
||||||
|
bots:
|
||||||
|
# Pathological bots to deny
|
||||||
|
- # 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
|
||||||
|
|
||||||
|
# Aggressively block AI/LLM related bots/agents by default
|
||||||
|
- import: (data)/meta/ai-block-aggressive.yaml
|
||||||
|
|
||||||
|
# Consider replacing the aggressive AI policy with more selective policies:
|
||||||
|
# - import: (data)/meta/ai-block-moderate.yaml
|
||||||
|
# - import: (data)/meta/ai-block-permissive.yaml
|
||||||
|
|
||||||
|
# Search engine crawlers to allow, defaults to:
|
||||||
|
# - Google (so they don't try to bypass Anubis)
|
||||||
|
# - Apple
|
||||||
|
# - Bing
|
||||||
|
# - DuckDuckGo
|
||||||
|
# - Qwant
|
||||||
|
# - The Internet Archive
|
||||||
|
# - Kagi
|
||||||
|
# - Marginalia
|
||||||
|
# - Mojeek
|
||||||
|
- import: (data)/crawlers/_allow-good.yaml
|
||||||
|
# Challenge Firefox AI previews
|
||||||
|
- import: (data)/clients/x-firefox-ai.yaml
|
||||||
|
|
||||||
|
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
|
||||||
|
- import: (data)/common/keep-internet-working.yaml
|
||||||
|
|
||||||
|
# # Punish any bot with "bot" in the user-agent string
|
||||||
|
# # This is known to have a high false-positive rate, use at your own risk
|
||||||
|
# - name: generic-bot-catchall
|
||||||
|
# user_agent_regex: (?i:bot|crawler)
|
||||||
|
# action: CHALLENGE
|
||||||
|
# challenge:
|
||||||
|
# difficulty: 16 # impossible
|
||||||
|
# report_as: 4 # lie to the operator
|
||||||
|
# algorithm: slow # intentionally waste CPU cycles and time
|
||||||
|
|
||||||
|
# Generic catchall rule
|
||||||
|
- name: generic-browser
|
||||||
|
user_agent_regex: >-
|
||||||
|
Mozilla|Opera
|
||||||
|
action: CHALLENGE
|
||||||
|
challenge:
|
||||||
|
difficulty: 1 # Number of seconds to wait before refreshing the page
|
||||||
|
report_as: 4 # Unused by this challenge method
|
||||||
|
algorithm: metarefresh # Specify a non-JS challenge method
|
||||||
|
|
||||||
|
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
|
||||||
@@ -11,48 +11,58 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: anubis-docs
|
app: anubis-docs
|
||||||
spec:
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: anubis
|
||||||
|
configMap:
|
||||||
|
name: anubis-cfg
|
||||||
containers:
|
containers:
|
||||||
- name: anubis-docs
|
- name: anubis-docs
|
||||||
image: ghcr.io/techarohq/anubis/docs:main
|
image: ghcr.io/techarohq/anubis/docs:main
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "128Mi"
|
memory: "128Mi"
|
||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
ports:
|
requests:
|
||||||
- containerPort: 80
|
cpu: 250m
|
||||||
- name: anubis
|
memory: 128Mi
|
||||||
image: ghcr.io/techarohq/anubis:main
|
ports:
|
||||||
imagePullPolicy: Always
|
- containerPort: 80
|
||||||
env:
|
- name: anubis
|
||||||
- name: "BIND"
|
image: ghcr.io/techarohq/anubis:main
|
||||||
value: ":8081"
|
imagePullPolicy: Always
|
||||||
- name: "DIFFICULTY"
|
env:
|
||||||
value: "4"
|
- name: "BIND"
|
||||||
- name: "METRICS_BIND"
|
value: ":8081"
|
||||||
value: ":9090"
|
- name: "DIFFICULTY"
|
||||||
- name: "POLICY_FNAME"
|
value: "4"
|
||||||
value: ""
|
- name: "METRICS_BIND"
|
||||||
- name: "SERVE_ROBOTS_TXT"
|
value: ":9090"
|
||||||
value: "false"
|
- name: "POLICY_FNAME"
|
||||||
- name: "TARGET"
|
value: "/xe/cfg/anubis/botPolicies.yaml"
|
||||||
value: "http://localhost:80"
|
- name: "SERVE_ROBOTS_TXT"
|
||||||
# - name: "SLOG_LEVEL"
|
value: "false"
|
||||||
# value: "debug"
|
- name: "TARGET"
|
||||||
resources:
|
value: "http://localhost:80"
|
||||||
limits:
|
# - name: "SLOG_LEVEL"
|
||||||
cpu: 500m
|
# value: "debug"
|
||||||
memory: 128Mi
|
volumeMounts:
|
||||||
requests:
|
- name: anubis
|
||||||
cpu: 250m
|
mountPath: /xe/cfg/anubis
|
||||||
memory: 128Mi
|
resources:
|
||||||
securityContext:
|
limits:
|
||||||
runAsUser: 1000
|
cpu: 500m
|
||||||
runAsGroup: 1000
|
memory: 128Mi
|
||||||
runAsNonRoot: true
|
requests:
|
||||||
allowPrivilegeEscalation: false
|
cpu: 250m
|
||||||
capabilities:
|
memory: 128Mi
|
||||||
drop:
|
securityContext:
|
||||||
- ALL
|
runAsUser: 1000
|
||||||
seccompProfile:
|
runAsGroup: 1000
|
||||||
type: RuntimeDefault
|
runAsNonRoot: true
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
|||||||
@@ -2,4 +2,10 @@ resources:
|
|||||||
- deployment.yaml
|
- deployment.yaml
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
- onionservice.yaml
|
- onionservice.yaml
|
||||||
- service.yaml
|
- service.yaml
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: anubis-cfg
|
||||||
|
behavior: create
|
||||||
|
files:
|
||||||
|
- ./cfg/anubis/botPolicies.yaml
|
||||||
|
|||||||
BIN
docs/static/img/sponsors/raptor-computing-logo.webp
vendored
Normal file
BIN
docs/static/img/sponsors/raptor-computing-logo.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
docs/static/img/sponsors/uberspace-logo.webp
vendored
Normal file
BIN
docs/static/img/sponsors/uberspace-logo.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
docs/static/img/sponsors/wildbase-logo.webp
vendored
Normal file
BIN
docs/static/img/sponsors/wildbase-logo.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
30
go.mod
30
go.mod
@@ -3,7 +3,7 @@ module github.com/TecharoHQ/anubis
|
|||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.3.865
|
github.com/a-h/templ v0.3.898
|
||||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/cel-go v0.25.0
|
github.com/google/cel-go v0.25.0
|
||||||
@@ -11,23 +11,23 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||||
github.com/yl2chen/cidranger v1.0.2
|
github.com/yl2chen/cidranger v1.0.2
|
||||||
golang.org/x/net v0.40.0
|
golang.org/x/net v0.41.0
|
||||||
k8s.io/apimachinery v0.33.0
|
k8s.io/apimachinery v0.33.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||||
cel.dev/expr v0.23.1 // indirect
|
cel.dev/expr v0.23.1 // indirect
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||||
github.com/Songmu/gitconfig v0.2.0 // indirect
|
github.com/Songmu/gitconfig v0.2.0 // indirect
|
||||||
github.com/TecharoHQ/yeet v0.2.3 // indirect
|
github.com/TecharoHQ/yeet v0.6.0 // indirect
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
@@ -40,7 +40,7 @@ require (
|
|||||||
github.com/cli/go-gh v0.1.0 // indirect
|
github.com/cli/go-gh v0.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.0 // indirect
|
github.com/cloudflare/circl v1.6.0 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0 // indirect
|
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
|
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
@@ -59,11 +59,11 @@ require (
|
|||||||
github.com/goccy/go-yaml v1.12.0 // indirect
|
github.com/goccy/go-yaml v1.12.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||||
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect
|
github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/goreleaser/chglog v0.7.0 // indirect
|
github.com/goreleaser/chglog v0.7.0 // indirect
|
||||||
github.com/goreleaser/fileglob v1.3.0 // indirect
|
github.com/goreleaser/fileglob v1.3.0 // indirect
|
||||||
github.com/goreleaser/nfpm/v2 v2.42.0 // indirect
|
github.com/goreleaser/nfpm/v2 v2.42.1 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
@@ -88,15 +88,16 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
||||||
golang.org/x/crypto v0.38.0 // indirect
|
golang.org/x/crypto v0.39.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
|
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
golang.org/x/sync v0.14.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
|
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/term v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.32.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
golang.org/x/vuln v1.1.4 // indirect
|
golang.org/x/vuln v1.1.4 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
||||||
@@ -105,6 +106,7 @@ require (
|
|||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
honnef.co/go/tools v0.6.1 // indirect
|
honnef.co/go/tools v0.6.1 // indirect
|
||||||
|
mvdan.cc/sh/v3 v3.11.0 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
62
go.sum
62
go.sum
@@ -2,8 +2,8 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeX
|
|||||||
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||||
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
|
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
|
||||||
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
||||||
@@ -20,20 +20,20 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
|
|||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
|
||||||
github.com/Songmu/gitconfig v0.2.0 h1:pX2++u4KUq+K2k/ZCzGXLtkD3ceCqIdi0tDyb+IbSyo=
|
github.com/Songmu/gitconfig v0.2.0 h1:pX2++u4KUq+K2k/ZCzGXLtkD3ceCqIdi0tDyb+IbSyo=
|
||||||
github.com/Songmu/gitconfig v0.2.0/go.mod h1:cB5bYJer+pl7W8g6RHFwL/0X6aJROVrYuHlvc7PT+hE=
|
github.com/Songmu/gitconfig v0.2.0/go.mod h1:cB5bYJer+pl7W8g6RHFwL/0X6aJROVrYuHlvc7PT+hE=
|
||||||
github.com/TecharoHQ/yeet v0.2.3 h1:Pcsnq5HTnk4Xntlu/FNEidH7x55bIx+f5Mk1hpVIngs=
|
github.com/TecharoHQ/yeet v0.6.0 h1:RCBAjr7wIlllsgy0tpvWpLX7jsZgu2tiuBY3RrprcR0=
|
||||||
github.com/TecharoHQ/yeet v0.2.3/go.mod h1:avLiwxZpNY37A/o35XledvdmGnTkm3G7+Oskxca6Z7Y=
|
github.com/TecharoHQ/yeet v0.6.0/go.mod h1:bj2V4Fg8qKQXoiuPZa3HuawrE8g+LsOQv/9q2WyGSsA=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||||
github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A=
|
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
|
||||||
github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
@@ -65,14 +65,16 @@ github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5
|
|||||||
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
|
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
|
||||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
|
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
|
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
|
||||||
@@ -117,6 +119,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
|||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||||
@@ -141,8 +145,8 @@ github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE
|
|||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a h1:JJBdjSfqSy3mnDT0940ASQFghwcZ4y4cb6ttjAoXqwE=
|
github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc h1:qES+d3PvR9CN+zARQQH/bNXH0ybzmdjNMHICrBwXD28=
|
||||||
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
|
github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -153,8 +157,8 @@ github.com/goreleaser/chglog v0.7.0 h1:/KzXWAeg4DrEz4r3OI6K2Yb8RAsVGeInCUfLWFXL9
|
|||||||
github.com/goreleaser/chglog v0.7.0/go.mod h1:2h/yyq9xvTUeM9tOoucBP+jri8Dj28splx+SjlYkklc=
|
github.com/goreleaser/chglog v0.7.0/go.mod h1:2h/yyq9xvTUeM9tOoucBP+jri8Dj28splx+SjlYkklc=
|
||||||
github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I=
|
github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I=
|
||||||
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
|
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
|
||||||
github.com/goreleaser/nfpm/v2 v2.42.0 h1:7BW4WQWyvZDrT0C7SyWop+J8rtqFyTB17Sb2/j/NxMI=
|
github.com/goreleaser/nfpm/v2 v2.42.1 h1:xu2pLRgQuz2ab+YZFoeIzwU/M5jjjCKDGwv1lRbVGvk=
|
||||||
github.com/goreleaser/nfpm/v2 v2.42.0/go.mod h1:DtNL+nKpfB8sMFZp+X7Xu3W64atyZYtTnYe8O925/mg=
|
github.com/goreleaser/nfpm/v2 v2.42.1/go.mod h1:dY53KWYKebkOocxgkmpM7SRX0Nv5hU+jEu2kIaM4/LI=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
|
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
@@ -270,16 +274,16 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
|
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -287,13 +291,13 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -334,14 +338,14 @@ 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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
|
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/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -371,8 +375,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||||
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
||||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
|
||||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||||
|
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
|
||||||
|
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
|
||||||
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
|
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
|
||||||
pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE=
|
pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE=
|
||||||
pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
|
pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
|
||||||
|
|||||||
35
internal/gzip.go
Normal file
35
internal/gzip.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GzipMiddleware(level int, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
gz, err := gzip.NewWriterLevel(w, level)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer gz.Close()
|
||||||
|
|
||||||
|
grw := gzipResponseWriter{ResponseWriter: w, sink: gz}
|
||||||
|
next.ServeHTTP(grw, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
sink *gzip.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
return w.sink.Write(b)
|
||||||
|
}
|
||||||
@@ -81,12 +81,12 @@ func XForwardedForToXRealIP(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// XForwardedForUpdate sets or updates the X-Forwarded-For header, adding
|
// XForwardedForUpdate sets or updates the X-Forwarded-For header, adding
|
||||||
// the known remote address to an existing chain if present
|
// the known remote address to an existing chain if present
|
||||||
func XForwardedForUpdate(next http.Handler) http.Handler {
|
func XForwardedForUpdate(stripPrivate bool, next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer next.ServeHTTP(w, r)
|
defer next.ServeHTTP(w, r)
|
||||||
|
|
||||||
pref := XFFComputePreferences{
|
pref := XFFComputePreferences{
|
||||||
StripPrivate: true,
|
StripPrivate: stripPrivate,
|
||||||
StripLoopback: true,
|
StripLoopback: true,
|
||||||
StripCGNAT: true,
|
StripCGNAT: true,
|
||||||
Flatten: true,
|
Flatten: true,
|
||||||
|
|||||||
7
internal/mimetype.go
Normal file
7
internal/mimetype.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "mime"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mime.AddExtensionType(".mjs", "text/javascript")
|
||||||
|
}
|
||||||
@@ -214,6 +214,11 @@ func TestPlaywrightBrowser(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SKIP_INTEGRATION") != "" {
|
||||||
|
t.Skip("SKIP_INTEGRATION was set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
startPlaywright(t)
|
startPlaywright(t)
|
||||||
|
|
||||||
pw := setupPlaywright(t)
|
pw := setupPlaywright(t)
|
||||||
@@ -289,6 +294,11 @@ func TestPlaywrightWithBasePrefix(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SKIP_INTEGRATION") != "" {
|
||||||
|
t.Skip("SKIP_INTEGRATION was set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
t.Skip("NOTE(Xe)\\ these tests require HTTPS support in #364")
|
t.Skip("NOTE(Xe)\\ these tests require HTTPS support in #364")
|
||||||
|
|
||||||
startPlaywright(t)
|
startPlaywright(t)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestXForwardedForUpdateIgnoreUnix(t *testing.T) {
|
|||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
XForwardedForUpdate(h).ServeHTTP(w, r)
|
XForwardedForUpdate(true, h).ServeHTTP(w, r)
|
||||||
|
|
||||||
if r.RemoteAddr != remoteAddr {
|
if r.RemoteAddr != remoteAddr {
|
||||||
t.Errorf("wanted remoteAddr to be %s, got: %s", r.RemoteAddr, remoteAddr)
|
t.Errorf("wanted remoteAddr to be %s, got: %s", r.RemoteAddr, remoteAddr)
|
||||||
@@ -43,7 +43,7 @@ func TestXForwardedForUpdateAddToChain(t *testing.T) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
srv := httptest.NewServer(XForwardedForUpdate(h))
|
srv := httptest.NewServer(XForwardedForUpdate(true, h))
|
||||||
|
|
||||||
r, err := http.NewRequest(http.MethodGet, srv.URL, nil)
|
r, err := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,6 +81,15 @@ func TestComputeXFFHeader(t *testing.T) {
|
|||||||
},
|
},
|
||||||
result: "1.1.1.1,127.0.0.1",
|
result: "1.1.1.1,127.0.0.1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "StripPrivate",
|
||||||
|
remoteAddr: "127.0.0.1:80",
|
||||||
|
origXFFHeader: "1.1.1.1,10.0.0.1",
|
||||||
|
pref: XFFComputePreferences{
|
||||||
|
StripPrivate: false,
|
||||||
|
},
|
||||||
|
result: "1.1.1.1,10.0.0.1,127.0.0.1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "StripLoopback",
|
name: "StripLoopback",
|
||||||
remoteAddr: "127.0.0.1:80",
|
remoteAddr: "127.0.0.1:80",
|
||||||
|
|||||||
304
lib/anubis.go
304
lib/anubis.go
@@ -3,16 +3,14 @@ package lib
|
|||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -26,36 +24,40 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
|
|
||||||
|
// challenge implementations
|
||||||
|
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
||||||
|
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
challengesIssued = promauto.NewCounter(prometheus.CounterOpts{
|
challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_challenges_issued",
|
Name: "anubis_challenges_issued",
|
||||||
Help: "The total number of challenges issued",
|
Help: "The total number of challenges issued",
|
||||||
})
|
}, []string{"method"})
|
||||||
|
|
||||||
challengesValidated = promauto.NewCounter(prometheus.CounterOpts{
|
challengesValidated = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_challenges_validated",
|
Name: "anubis_challenges_validated",
|
||||||
Help: "The total number of challenges validated",
|
Help: "The total number of challenges validated",
|
||||||
})
|
}, []string{"method"})
|
||||||
|
|
||||||
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_dronebl_hits",
|
Name: "anubis_dronebl_hits",
|
||||||
Help: "The total number of hits from DroneBL",
|
Help: "The total number of hits from DroneBL",
|
||||||
}, []string{"status"})
|
}, []string{"status"})
|
||||||
|
|
||||||
failedValidations = promauto.NewCounter(prometheus.CounterOpts{
|
failedValidations = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_failed_validations",
|
Name: "anubis_failed_validations",
|
||||||
Help: "The total number of failed validations",
|
Help: "The total number of failed validations",
|
||||||
})
|
}, []string{"method"})
|
||||||
|
|
||||||
timeTaken = promauto.NewHistogram(prometheus.HistogramOpts{
|
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_time_taken",
|
Name: "anubis_proxied_requests_total",
|
||||||
Help: "The time taken for a browser to generate a response (milliseconds)",
|
Help: "Number of requests proxied through Anubis to upstream targets",
|
||||||
Buckets: prometheus.ExponentialBucketsRange(1, math.Pow(2, 18), 19),
|
}, []string{"host"})
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -64,18 +66,23 @@ type Server struct {
|
|||||||
policy *policy.ParsedConfig
|
policy *policy.ParsedConfig
|
||||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||||
OGTags *ogtags.OGTagCache
|
OGTags *ogtags.OGTagCache
|
||||||
|
cookieName string
|
||||||
priv ed25519.PrivateKey
|
priv ed25519.PrivateKey
|
||||||
pub ed25519.PublicKey
|
pub ed25519.PublicKey
|
||||||
opts Options
|
opts Options
|
||||||
cookieName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
||||||
fp := sha256.Sum256(s.priv.Seed())
|
fp := sha256.Sum256(s.pub[:])
|
||||||
|
|
||||||
|
acceptLanguage := r.Header.Get("Accept-Language")
|
||||||
|
if len(acceptLanguage) > 5 {
|
||||||
|
acceptLanguage = acceptLanguage[:5]
|
||||||
|
}
|
||||||
|
|
||||||
challengeData := fmt.Sprintf(
|
challengeData := fmt.Sprintf(
|
||||||
"Accept-Language=%s,X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d",
|
"Accept-Language=%s,X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d",
|
||||||
r.Header.Get("Accept-Language"),
|
acceptLanguage,
|
||||||
r.Header.Get("X-Real-Ip"),
|
r.Header.Get("X-Real-Ip"),
|
||||||
r.UserAgent(),
|
r.UserAgent(),
|
||||||
time.Now().UTC().Round(24*7*time.Hour).Format(time.RFC3339),
|
time.Now().UTC().Round(24*7*time.Hour).Format(time.RFC3339),
|
||||||
@@ -96,6 +103,12 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
||||||
lg := internal.GetRequestLogger(r)
|
lg := internal.GetRequestLogger(r)
|
||||||
|
|
||||||
|
// Adjust cookie path if base prefix is not empty
|
||||||
|
cookiePath := "/"
|
||||||
|
if anubis.BasePrefix != "" {
|
||||||
|
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
|
||||||
|
}
|
||||||
|
|
||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("check failed", "err", err)
|
lg.Error("check failed", "err", err)
|
||||||
@@ -121,21 +134,21 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
|||||||
ckie, err := r.Cookie(s.cookieName)
|
ckie, err := r.Cookie(s.cookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Debug("cookie not found", "path", r.URL.Path)
|
lg.Debug("cookie not found", "path", r.URL.Path)
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ckie.Valid(); err != nil {
|
if err := ckie.Valid(); err != nil {
|
||||||
lg.Debug("cookie is invalid", "err", err)
|
lg.Debug("cookie is invalid", "err", err)
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
|
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
|
||||||
lg.Debug("cookie expired", "path", r.URL.Path)
|
lg.Debug("cookie expired", "path", r.URL.Path)
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -146,7 +159,30 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
|||||||
|
|
||||||
if err != nil || !token.Valid {
|
if err != nil || !token.Valid {
|
||||||
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
|
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
|
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
lg.Debug("invalid token claims type", "path", r.URL.Path)
|
||||||
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
|
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policyRule, ok := claims["policyRule"].(string)
|
||||||
|
if !ok {
|
||||||
|
lg.Debug("policyRule claim is not a string")
|
||||||
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
|
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyRule != rule.Hash() {
|
||||||
|
lg.Debug("user originally passed with a different rule, issuing new challenge", "old", policyRule, "new", rule.Name)
|
||||||
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -156,13 +192,19 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool {
|
func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool {
|
||||||
|
// Adjust cookie path if base prefix is not empty
|
||||||
|
cookiePath := "/"
|
||||||
|
if anubis.BasePrefix != "" {
|
||||||
|
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
|
||||||
|
}
|
||||||
|
|
||||||
switch cr.Rule {
|
switch cr.Rule {
|
||||||
case config.RuleAllow:
|
case config.RuleAllow:
|
||||||
lg.Debug("allowing traffic to origin (explicit)")
|
lg.Debug("allowing traffic to origin (explicit)")
|
||||||
s.ServeHTTPNext(w, r)
|
s.ServeHTTPNext(w, r)
|
||||||
return true
|
return true
|
||||||
case config.RuleDeny:
|
case config.RuleDeny:
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
lg.Info("explicit deny")
|
lg.Info("explicit deny")
|
||||||
if rule == nil {
|
if rule == nil {
|
||||||
lg.Error("rule is nil, cannot calculate checksum")
|
lg.Error("rule is nil, cannot calculate checksum")
|
||||||
@@ -181,7 +223,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
|
|||||||
s.RenderBench(w, r)
|
s.RenderBench(w, r)
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule)
|
slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule)
|
||||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"")
|
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"")
|
||||||
return true
|
return true
|
||||||
@@ -214,6 +256,21 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
|||||||
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := internal.GetRequestLogger(r)
|
lg := internal.GetRequestLogger(r)
|
||||||
|
|
||||||
|
redir := r.FormValue("redir")
|
||||||
|
if redir == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
lg.Error("invalid invocation of MakeChallenge", "redir", redir)
|
||||||
|
encoder.Encode(struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{
|
||||||
|
Error: "Invalid invocation of MakeChallenge",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.URL.Path = redir
|
||||||
|
|
||||||
encoder := json.NewEncoder(w)
|
encoder := json.NewEncoder(w)
|
||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -233,6 +290,8 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
lg = lg.With("check_result", cr)
|
lg = lg.With("check_result", cr)
|
||||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||||
|
|
||||||
|
s.SetCookie(w, anubis.TestCookieName, challenge, "/")
|
||||||
|
|
||||||
err = encoder.Encode(struct {
|
err = encoder.Encode(struct {
|
||||||
Rules *config.ChallengeRules `json:"rules"`
|
Rules *config.ChallengeRules `json:"rules"`
|
||||||
Challenge string `json:"challenge"`
|
Challenge string `json:"challenge"`
|
||||||
@@ -246,12 +305,28 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
lg.Debug("made challenge", "challenge", challenge, "rules", rule.Challenge, "cr", cr)
|
lg.Debug("made challenge", "challenge", challenge, "rules", rule.Challenge, "cr", cr)
|
||||||
challengesIssued.Inc()
|
challengesIssued.WithLabelValues("api").Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := internal.GetRequestLogger(r)
|
lg := internal.GetRequestLogger(r)
|
||||||
|
|
||||||
|
// Adjust cookie path if base prefix is not empty
|
||||||
|
cookiePath := "/"
|
||||||
|
if anubis.BasePrefix != "" {
|
||||||
|
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Cookie(anubis.TestCookieName); err == http.ErrNoCookie {
|
||||||
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
|
s.ClearCookie(w, anubis.TestCookieName, "/")
|
||||||
|
lg.Warn("user has cookies disabled, this is not an anubis bug")
|
||||||
|
s.respondWithError(w, r, "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ClearCookie(w, anubis.TestCookieName, "/")
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
redirURL, err := url.ParseRequestURI(redir)
|
redirURL, err := url.ParseRequestURI(redir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -262,42 +337,6 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
// used by the path checker rule
|
// used by the path checker rule
|
||||||
r.URL = redirURL
|
r.URL = redirURL
|
||||||
|
|
||||||
cr, rule, err := s.check(r)
|
|
||||||
if err != nil {
|
|
||||||
lg.Error("check failed", "err", err)
|
|
||||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".\"")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lg = lg.With("check_result", cr)
|
|
||||||
|
|
||||||
nonceStr := r.FormValue("nonce")
|
|
||||||
if nonceStr == "" {
|
|
||||||
s.ClearCookie(w)
|
|
||||||
lg.Debug("no nonce")
|
|
||||||
s.respondWithError(w, r, "missing nonce")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsedTimeStr := r.FormValue("elapsedTime")
|
|
||||||
if elapsedTimeStr == "" {
|
|
||||||
s.ClearCookie(w)
|
|
||||||
lg.Debug("no elapsedTime")
|
|
||||||
s.respondWithError(w, r, "missing elapsedTime")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
s.ClearCookie(w)
|
|
||||||
lg.Debug("elapsedTime doesn't parse", "err", err)
|
|
||||||
s.respondWithError(w, r, "invalid elapsedTime")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lg.Info("challenge took", "elapsedTime", elapsedTime)
|
|
||||||
timeTaken.Observe(elapsedTime)
|
|
||||||
|
|
||||||
response := r.FormValue("response")
|
|
||||||
urlParsed, err := r.URL.Parse(redir)
|
urlParsed, err := r.URL.Parse(redir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.respondWithError(w, r, "Redirect URL not parseable")
|
s.respondWithError(w, r, "Redirect URL not parseable")
|
||||||
@@ -308,85 +347,75 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
cr, rule, err := s.check(r)
|
||||||
|
|
||||||
nonce, err := strconv.Atoi(nonceStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.ClearCookie(w)
|
lg.Error("check failed", "err", err)
|
||||||
lg.Debug("nonce doesn't parse", "err", err)
|
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\"")
|
||||||
s.respondWithError(w, r, "invalid nonce")
|
return
|
||||||
|
}
|
||||||
|
lg = lg.With("check_result", cr)
|
||||||
|
|
||||||
|
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||||
|
if !ok {
|
||||||
|
lg.Error("check failed", "err", err)
|
||||||
|
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
calcString := fmt.Sprintf("%s%d", challenge, nonce)
|
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||||
calculated := internal.SHA256sum(calcString)
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
if err := impl.Validate(r, lg, rule, challengeStr); err != nil {
|
||||||
s.ClearCookie(w)
|
failedValidations.WithLabelValues(string(rule.Challenge.Algorithm)).Inc()
|
||||||
lg.Debug("hash does not match", "got", response, "want", calculated)
|
var cerr *challenge.Error
|
||||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
failedValidations.Inc()
|
lg.Debug("challenge validate call failed", "err", err)
|
||||||
return
|
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &cerr):
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, challenge.ErrFailed):
|
||||||
|
s.respondWithStatus(w, r, cerr.PublicReason, cerr.StatusCode)
|
||||||
|
case errors.Is(err, challenge.ErrInvalidFormat), errors.Is(err, challenge.ErrMissingField):
|
||||||
|
s.respondWithError(w, r, cerr.PublicReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compare the leading zeroes
|
|
||||||
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
|
||||||
s.ClearCookie(w)
|
|
||||||
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
|
||||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
|
||||||
failedValidations.Inc()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust cookie path if base prefix is not empty
|
|
||||||
cookiePath := "/"
|
|
||||||
if anubis.BasePrefix != "" {
|
|
||||||
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
|
|
||||||
}
|
|
||||||
// generate JWT cookie
|
// generate JWT cookie
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
|
tokenString, err := s.signJWT(jwt.MapClaims{
|
||||||
"challenge": challenge,
|
"challenge": challengeStr,
|
||||||
"nonce": nonceStr,
|
"method": rule.Challenge.Algorithm,
|
||||||
"response": response,
|
"policyRule": rule.Hash(),
|
||||||
"iat": time.Now().Unix(),
|
"action": string(cr.Rule),
|
||||||
"nbf": time.Now().Add(-1 * time.Minute).Unix(),
|
|
||||||
"exp": time.Now().Add(s.opts.CookieExpiration).Unix(),
|
|
||||||
})
|
})
|
||||||
tokenString, err := token.SignedString(s.priv)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("failed to sign JWT", "err", err)
|
lg.Error("failed to sign JWT", "err", err)
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
s.respondWithError(w, r, "failed to sign JWT")
|
s.respondWithError(w, r, "failed to sign JWT")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
s.SetCookie(w, s.cookieName, tokenString, cookiePath)
|
||||||
Name: s.cookieName,
|
|
||||||
Value: tokenString,
|
|
||||||
Expires: time.Now().Add(s.opts.CookieExpiration),
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Domain: s.opts.CookieDomain,
|
|
||||||
Partitioned: s.opts.CookiePartitioned,
|
|
||||||
Path: cookiePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
challengesValidated.Inc()
|
challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
||||||
lg.Debug("challenge passed, redirecting to app")
|
lg.Debug("challenge passed, redirecting to app")
|
||||||
http.Redirect(w, r, redir, http.StatusFound)
|
http.Redirect(w, r, redir, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) TestError(w http.ResponseWriter, r *http.Request) {
|
func cr(name string, rule config.Rule, weight int) policy.CheckResult {
|
||||||
err := r.FormValue("err")
|
|
||||||
s.respondWithError(w, r, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cr(name string, rule config.Rule) policy.CheckResult {
|
|
||||||
return policy.CheckResult{
|
return policy.CheckResult{
|
||||||
Name: name,
|
Name: name,
|
||||||
Rule: rule,
|
Rule: rule,
|
||||||
|
Weight: weight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
weightOkayStatic = policy.NewStaticHashChecker("weight/okay")
|
||||||
|
weightMildSusStatic = policy.NewStaticHashChecker("weight/mild-suspicion")
|
||||||
|
weightVerySusStatic = policy.NewStaticHashChecker("weight/extreme-suspicion")
|
||||||
|
)
|
||||||
|
|
||||||
// Check evaluates the list of rules, and returns the result
|
// Check evaluates the list of rules, and returns the result
|
||||||
func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error) {
|
func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error) {
|
||||||
host := r.Header.Get("X-Real-Ip")
|
host := r.Header.Get("X-Real-Ip")
|
||||||
@@ -399,6 +428,8 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
|
|||||||
return decaymap.Zilch[policy.CheckResult](), nil, fmt.Errorf("[misconfiguration] %q is not an IP address", host)
|
return decaymap.Zilch[policy.CheckResult](), nil, fmt.Errorf("[misconfiguration] %q is not an IP address", host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
weight := 0
|
||||||
|
|
||||||
for _, b := range s.policy.Bots {
|
for _, b := range s.policy.Bots {
|
||||||
match, err := b.Rules.Check(r)
|
match, err := b.Rules.Check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -406,16 +437,53 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if match {
|
if match {
|
||||||
return cr("bot/"+b.Name, b.Action), &b, nil
|
switch b.Action {
|
||||||
|
case config.RuleDeny, config.RuleAllow, config.RuleBenchmark, config.RuleChallenge:
|
||||||
|
return cr("bot/"+b.Name, b.Action, weight), &b, nil
|
||||||
|
case config.RuleWeigh:
|
||||||
|
slog.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust)
|
||||||
|
weight += b.Weight.Adjust
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cr("default/allow", config.RuleAllow), &policy.Bot{
|
switch {
|
||||||
|
case weight <= 0:
|
||||||
|
return cr("weight/okay", config.RuleAllow, weight), &policy.Bot{
|
||||||
|
Challenge: &config.ChallengeRules{
|
||||||
|
Difficulty: s.policy.DefaultDifficulty,
|
||||||
|
ReportAs: s.policy.DefaultDifficulty,
|
||||||
|
Algorithm: config.DefaultAlgorithm,
|
||||||
|
},
|
||||||
|
Rules: weightOkayStatic,
|
||||||
|
}, nil
|
||||||
|
case weight > 0 && weight < 10:
|
||||||
|
return cr("weight/mild-suspicion", config.RuleChallenge, weight), &policy.Bot{
|
||||||
|
Challenge: &config.ChallengeRules{
|
||||||
|
Difficulty: s.policy.DefaultDifficulty,
|
||||||
|
ReportAs: s.policy.DefaultDifficulty,
|
||||||
|
Algorithm: "metarefresh",
|
||||||
|
},
|
||||||
|
Rules: weightMildSusStatic,
|
||||||
|
}, nil
|
||||||
|
case weight >= 10:
|
||||||
|
return cr("weight/extreme-suspicion", config.RuleChallenge, weight), &policy.Bot{
|
||||||
|
Challenge: &config.ChallengeRules{
|
||||||
|
Difficulty: s.policy.DefaultDifficulty,
|
||||||
|
ReportAs: s.policy.DefaultDifficulty,
|
||||||
|
Algorithm: "fast",
|
||||||
|
},
|
||||||
|
Rules: weightVerySusStatic,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cr("default/allow", config.RuleAllow, weight), &policy.Bot{
|
||||||
Challenge: &config.ChallengeRules{
|
Challenge: &config.ChallengeRules{
|
||||||
Difficulty: s.policy.DefaultDifficulty,
|
Difficulty: s.policy.DefaultDifficulty,
|
||||||
ReportAs: s.policy.DefaultDifficulty,
|
ReportAs: s.policy.DefaultDifficulty,
|
||||||
Algorithm: config.AlgorithmFast,
|
Algorithm: config.DefaultAlgorithm,
|
||||||
},
|
},
|
||||||
|
Rules: &policy.CheckerList{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,6 +19,10 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
internal.InitSlog("debug")
|
||||||
|
}
|
||||||
|
|
||||||
func loadPolicies(t *testing.T, fname string) *policy.ParsedConfig {
|
func loadPolicies(t *testing.T, fname string) *policy.ParsedConfig {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -39,20 +45,29 @@ func spawnAnubis(t *testing.T, opts Options) *Server {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
type challenge struct {
|
type challengeResp struct {
|
||||||
Challenge string `json:"challenge"`
|
Challenge string `json:"challenge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeChallenge(t *testing.T, ts *httptest.Server) challenge {
|
func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challengeResp {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
resp, err := ts.Client().Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
req, err := http.NewRequest(http.MethodPost, ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't make request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("redir", "/")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := cli.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't request challenge: %v", err)
|
t.Fatalf("can't request challenge: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var chall challenge
|
var chall challengeResp
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||||
t.Fatalf("can't read challenge response body: %v", err)
|
t.Fatalf("can't read challenge response body: %v", err)
|
||||||
}
|
}
|
||||||
@@ -60,6 +75,86 @@ func makeChallenge(t *testing.T, ts *httptest.Server) challenge {
|
|||||||
return chall
|
return chall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.Client, chall challengeResp) *http.Response {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
nonce := 0
|
||||||
|
elapsedTime := 420
|
||||||
|
redir := "/"
|
||||||
|
calculated := ""
|
||||||
|
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
||||||
|
calculated = internal.SHA256sum(calcString)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't make request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("response", calculated)
|
||||||
|
q.Set("nonce", fmt.Sprint(nonce))
|
||||||
|
q.Set("redir", redir)
|
||||||
|
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := cli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't do request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggingCookieJar struct {
|
||||||
|
t *testing.T
|
||||||
|
lock sync.Mutex
|
||||||
|
cookies map[string][]*http.Cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcj *loggingCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||||
|
lcj.lock.Lock()
|
||||||
|
defer lcj.lock.Unlock()
|
||||||
|
|
||||||
|
// XXX(Xe): This is not RFC compliant in the slightest.
|
||||||
|
result, ok := lcj.cookies[u.Host]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lcj.t.Logf("requested cookies for %s", u)
|
||||||
|
|
||||||
|
for _, ckie := range result {
|
||||||
|
lcj.t.Logf("get cookie: <- %s", ckie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcj *loggingCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||||
|
lcj.lock.Lock()
|
||||||
|
defer lcj.lock.Unlock()
|
||||||
|
|
||||||
|
for _, ckie := range cookies {
|
||||||
|
lcj.t.Logf("set cookie: %s -> %s", u, ckie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX(Xe): This is not RFC compliant in the slightest.
|
||||||
|
lcj.cookies[u.Host] = append(lcj.cookies[u.Host], cookies...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpClient(t *testing.T) *http.Client {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cli := &http.Client{
|
||||||
|
Jar: &loggingCookieJar{t: t, cookies: map[string][]*http.Cookie{}},
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadPolicies(t *testing.T) {
|
func TestLoadPolicies(t *testing.T) {
|
||||||
for _, fname := range []string{"botPolicies.json", "botPolicies.yaml"} {
|
for _, fname := range []string{"botPolicies.json", "botPolicies.yaml"} {
|
||||||
t.Run(fname, func(t *testing.T) {
|
t.Run(fname, func(t *testing.T) {
|
||||||
@@ -85,42 +180,15 @@ func TestCVE2025_24369(t *testing.T) {
|
|||||||
Next: http.NewServeMux(),
|
Next: http.NewServeMux(),
|
||||||
Policy: pol,
|
Policy: pol,
|
||||||
|
|
||||||
CookieDomain: ".local.cetacean.club",
|
CookieName: t.Name(),
|
||||||
CookiePartitioned: true,
|
|
||||||
CookieName: t.Name(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
chall := makeChallenge(t, ts)
|
cli := httpClient(t)
|
||||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, 0)
|
chall := makeChallenge(t, ts, cli)
|
||||||
calculated := internal.SHA256sum(calcString)
|
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||||
nonce := 0
|
|
||||||
elapsedTime := 420
|
|
||||||
redir := "/"
|
|
||||||
|
|
||||||
cli := ts.Client()
|
|
||||||
cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
q := req.URL.Query()
|
|
||||||
q.Set("response", calculated)
|
|
||||||
q.Set("nonce", fmt.Sprint(nonce))
|
|
||||||
q.Set("redir", redir)
|
|
||||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
resp, err := cli.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't do challenge passing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusFound {
|
if resp.StatusCode == http.StatusFound {
|
||||||
t.Log("Regression on CVE-2025-24369")
|
t.Log("Regression on CVE-2025-24369")
|
||||||
@@ -137,58 +205,18 @@ func TestCookieCustomExpiration(t *testing.T) {
|
|||||||
Next: http.NewServeMux(),
|
Next: http.NewServeMux(),
|
||||||
Policy: pol,
|
Policy: pol,
|
||||||
|
|
||||||
CookieDomain: "local.cetacean.club",
|
|
||||||
CookieName: t.Name(),
|
|
||||||
CookieExpiration: ckieExpiration,
|
CookieExpiration: ckieExpiration,
|
||||||
})
|
})
|
||||||
|
|
||||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
cli := &http.Client{
|
cli := httpClient(t)
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
chall := makeChallenge(t, ts, cli)
|
||||||
return http.ErrUseLastResponse
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
requestReceiveLowerBound := time.Now().Add(-1 * time.Minute)
|
||||||
if err != nil {
|
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||||
t.Fatalf("can't request challenge: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var chall = struct {
|
|
||||||
Challenge string `json:"challenge"`
|
|
||||||
}{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
|
||||||
t.Fatalf("can't read challenge response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce := 0
|
|
||||||
elapsedTime := 420
|
|
||||||
redir := "/"
|
|
||||||
calculated := ""
|
|
||||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
|
||||||
calculated = internal.SHA256sum(calcString)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
q := req.URL.Query()
|
|
||||||
q.Set("response", calculated)
|
|
||||||
q.Set("nonce", fmt.Sprint(nonce))
|
|
||||||
q.Set("redir", redir)
|
|
||||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
requestReceiveLowerBound := time.Now()
|
|
||||||
resp, err = cli.Do(req)
|
|
||||||
requestReceiveUpperBound := time.Now()
|
requestReceiveUpperBound := time.Now()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't do challenge passing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusFound {
|
if resp.StatusCode != http.StatusFound {
|
||||||
resp.Write(os.Stderr)
|
resp.Write(os.Stderr)
|
||||||
@@ -226,59 +254,21 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
Next: http.NewServeMux(),
|
Next: http.NewServeMux(),
|
||||||
Policy: pol,
|
Policy: pol,
|
||||||
|
|
||||||
CookieDomain: "local.cetacean.club",
|
CookieDomain: "127.0.0.1",
|
||||||
CookiePartitioned: true,
|
CookiePartitioned: true,
|
||||||
CookieName: t.Name(),
|
CookieName: t.Name(),
|
||||||
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
requestReceiveLowerBound := time.Now()
|
||||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
cli := &http.Client{
|
cli := httpClient(t)
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
chall := makeChallenge(t, ts, cli)
|
||||||
return http.ErrUseLastResponse
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't request challenge: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var chall = struct {
|
|
||||||
Challenge string `json:"challenge"`
|
|
||||||
}{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
|
||||||
t.Fatalf("can't read challenge response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce := 0
|
|
||||||
elapsedTime := 420
|
|
||||||
redir := "/"
|
|
||||||
calculated := ""
|
|
||||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
|
||||||
calculated = internal.SHA256sum(calcString)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
q := req.URL.Query()
|
|
||||||
q.Set("response", calculated)
|
|
||||||
q.Set("nonce", fmt.Sprint(nonce))
|
|
||||||
q.Set("redir", redir)
|
|
||||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
requestReceiveLowerBound := time.Now()
|
|
||||||
resp, err = cli.Do(req)
|
|
||||||
requestReceiveUpperBound := time.Now()
|
requestReceiveUpperBound := time.Now()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't do challenge passing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusFound {
|
if resp.StatusCode != http.StatusFound {
|
||||||
resp.Write(os.Stderr)
|
resp.Write(os.Stderr)
|
||||||
@@ -298,8 +288,8 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ckie.Domain != "local.cetacean.club" {
|
if ckie.Domain != "127.0.0.1" {
|
||||||
t.Errorf("cookie domain is wrong, wanted local.cetacean.club, got: %s", ckie.Domain)
|
t.Errorf("cookie domain is wrong, wanted 127.0.0.1, got: %s", ckie.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
expirationLowerBound := requestReceiveLowerBound.Add(anubis.CookieDefaultExpirationTime)
|
expirationLowerBound := requestReceiveLowerBound.Add(anubis.CookieDefaultExpirationTime)
|
||||||
@@ -373,7 +363,7 @@ func TestBasePrefix(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no prefix",
|
name: "no prefix",
|
||||||
basePrefix: "",
|
basePrefix: "/",
|
||||||
path: "/.within.website/x/cmd/anubis/api/make-challenge",
|
path: "/.within.website/x/cmd/anubis/api/make-challenge",
|
||||||
expected: "/.within.website/x/cmd/anubis/api/make-challenge",
|
expected: "/.within.website/x/cmd/anubis/api/make-challenge",
|
||||||
},
|
},
|
||||||
@@ -408,8 +398,19 @@ func TestBasePrefix(t *testing.T) {
|
|||||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
|
cli := httpClient(t)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, ts.URL+tc.path, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("redir", tc.basePrefix)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
// Test API endpoint with prefix
|
// Test API endpoint with prefix
|
||||||
resp, err := ts.Client().Post(ts.URL+tc.path, "", nil)
|
resp, err := cli.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't request challenge: %v", err)
|
t.Fatalf("can't request challenge: %v", err)
|
||||||
}
|
}
|
||||||
@@ -419,7 +420,7 @@ func TestBasePrefix(t *testing.T) {
|
|||||||
t.Errorf("expected status code %d, got: %d", http.StatusOK, resp.StatusCode)
|
t.Errorf("expected status code %d, got: %d", http.StatusOK, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var chall challenge
|
var chall challengeResp
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||||
t.Fatalf("can't read challenge response body: %v", err)
|
t.Fatalf("can't read challenge response body: %v", err)
|
||||||
}
|
}
|
||||||
@@ -443,7 +444,6 @@ func TestBasePrefix(t *testing.T) {
|
|||||||
elapsedTime := 420
|
elapsedTime := 420
|
||||||
redir := "/"
|
redir := "/"
|
||||||
|
|
||||||
cli := ts.Client()
|
|
||||||
cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
@@ -452,12 +452,16 @@ func TestBasePrefix(t *testing.T) {
|
|||||||
passChallengePath := tc.path
|
passChallengePath := tc.path
|
||||||
passChallengePath = passChallengePath[:strings.LastIndex(passChallengePath, "/")+1] + "pass-challenge"
|
passChallengePath = passChallengePath[:strings.LastIndex(passChallengePath, "/")+1] + "pass-challenge"
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, ts.URL+passChallengePath, nil)
|
req, err = http.NewRequest(http.MethodGet, ts.URL+passChallengePath, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't make request: %v", err)
|
t.Fatalf("can't make request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := req.URL.Query()
|
for _, ckie := range resp.Cookies() {
|
||||||
|
req.AddCookie(ckie)
|
||||||
|
}
|
||||||
|
|
||||||
|
q = req.URL.Query()
|
||||||
q.Set("response", calculated)
|
q.Set("response", calculated)
|
||||||
q.Set("nonce", fmt.Sprint(nonce))
|
q.Set("nonce", fmt.Sprint(nonce))
|
||||||
q.Set("redir", redir)
|
q.Set("redir", redir)
|
||||||
@@ -561,6 +565,25 @@ func TestCloudflareWorkersRule(t *testing.T) {
|
|||||||
t.Fatalf("can't construct libanubis.Server: %v", err)
|
t.Fatalf("can't construct libanubis.Server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("with-cf-worker-header", func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("X-Real-Ip", "127.0.0.1")
|
||||||
|
req.Header.Add("Cf-Worker", "true")
|
||||||
|
|
||||||
|
cr, _, err := s.check(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cr.Rule != config.RuleDeny {
|
||||||
|
t.Errorf("rule is wrong, wanted %s, got: %s", config.RuleDeny, cr.Rule)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("no-cf-worker-header", func(t *testing.T) {
|
t.Run("no-cf-worker-header", func(t *testing.T) {
|
||||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -581,3 +604,31 @@ func TestCloudflareWorkersRule(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRuleChange(t *testing.T) {
|
||||||
|
pol := loadPolicies(t, "testdata/rule_change.yaml")
|
||||||
|
pol.DefaultDifficulty = 0
|
||||||
|
ckieExpiration := 10 * time.Minute
|
||||||
|
|
||||||
|
srv := spawnAnubis(t, Options{
|
||||||
|
Next: http.NewServeMux(),
|
||||||
|
Policy: pol,
|
||||||
|
|
||||||
|
CookieDomain: "127.0.0.1",
|
||||||
|
CookieName: t.Name(),
|
||||||
|
CookieExpiration: ckieExpiration,
|
||||||
|
})
|
||||||
|
|
||||||
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cli := httpClient(t)
|
||||||
|
|
||||||
|
chall := makeChallenge(t, ts, cli)
|
||||||
|
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusFound {
|
||||||
|
resp.Write(os.Stderr)
|
||||||
|
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
52
lib/challenge/challenge.go
Normal file
52
lib/challenge/challenge.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
registry map[string]Impl = map[string]Impl{}
|
||||||
|
regLock sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(name string, impl Impl) {
|
||||||
|
regLock.Lock()
|
||||||
|
defer regLock.Unlock()
|
||||||
|
|
||||||
|
registry[name] = impl
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(name string) (Impl, bool) {
|
||||||
|
regLock.RLock()
|
||||||
|
defer regLock.RUnlock()
|
||||||
|
result, ok := registry[name]
|
||||||
|
return result, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func Methods() []string {
|
||||||
|
regLock.RLock()
|
||||||
|
defer regLock.RUnlock()
|
||||||
|
var result []string
|
||||||
|
for method := range registry {
|
||||||
|
result = append(result, method)
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type Impl interface {
|
||||||
|
// Setup registers any additional routes with the Impl for assets or API routes.
|
||||||
|
Setup(mux *http.ServeMux)
|
||||||
|
|
||||||
|
// Issue a new challenge to the user, called by the Anubis.
|
||||||
|
Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error)
|
||||||
|
|
||||||
|
// Validate a challenge, making sure that it passes muster.
|
||||||
|
Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error
|
||||||
|
}
|
||||||
37
lib/challenge/error.go
Normal file
37
lib/challenge/error.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFailed = errors.New("challenge: user failed challenge")
|
||||||
|
ErrMissingField = errors.New("challenge: missing field")
|
||||||
|
ErrInvalidFormat = errors.New("challenge: field has invalid format")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewError(verb, publicReason string, privateReason error) *Error {
|
||||||
|
return &Error{
|
||||||
|
Verb: verb,
|
||||||
|
PublicReason: publicReason,
|
||||||
|
PrivateReason: privateReason,
|
||||||
|
StatusCode: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Verb string
|
||||||
|
PublicReason string
|
||||||
|
PrivateReason error
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("challenge: error when processing challenge: %s: %v", e.Verb, e.PrivateReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Unwrap() error {
|
||||||
|
return e.PrivateReason
|
||||||
|
}
|
||||||
53
lib/challenge/metarefresh/metarefresh.go
Normal file
53
lib/challenge/metarefresh/metarefresh.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package metarefresh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/web"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go tool github.com/a-h/templ/cmd/templ generate
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
challenge.Register("metarefresh", &Impl{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Impl struct{}
|
||||||
|
|
||||||
|
func (i *Impl) Setup(mux *http.ServeMux) {}
|
||||||
|
|
||||||
|
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error) {
|
||||||
|
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't render page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("redir", r.URL.String())
|
||||||
|
q.Set("challenge", challenge)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", page(challenge, u.String(), rule.Challenge.Difficulty), challenge, rule.Challenge, ogTags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't render page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return component, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, wantChallenge string) error {
|
||||||
|
gotChallenge := r.FormValue("challenge")
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare([]byte(wantChallenge), []byte(gotChallenge)) != 1 {
|
||||||
|
return challenge.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", challenge.ErrFailed, wantChallenge, gotChallenge))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
17
lib/challenge/metarefresh/metarefresh.templ
Normal file
17
lib/challenge/metarefresh/metarefresh.templ
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package metarefresh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ page(challenge, redir string, difficulty int) {
|
||||||
|
<div class="centered-div">
|
||||||
|
<img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
|
||||||
|
<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
|
||||||
|
<p id="status">Loading...</p>
|
||||||
|
<p>Please wait a moment while we ensure the security of your connection.</p>
|
||||||
|
<meta http-equiv="refresh" content={ fmt.Sprintf("%d; url=%s", difficulty, redir) }/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
85
lib/challenge/metarefresh/metarefresh_templ.go
Normal file
85
lib/challenge/metarefresh/metarefresh_templ.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.898
|
||||||
|
package metarefresh
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
)
|
||||||
|
|
||||||
|
func page(challenge, redir string, difficulty int) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 11, Col: 165}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"> <img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 174}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p id=\"status\">Loading...</p><p>Please wait a moment while we ensure the security of your connection.</p><meta http-equiv=\"refresh\" content=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 83}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
14
lib/challenge/metrics.go
Normal file
14
lib/challenge/metrics.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TimeTaken = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Name: "anubis_time_taken",
|
||||||
|
Help: "The time taken for a browser to generate a response (milliseconds)",
|
||||||
|
Buckets: prometheus.ExponentialBucketsRange(1, math.Pow(2, 20), 20),
|
||||||
|
}, []string{"method"})
|
||||||
83
lib/challenge/proofofwork/proofofwork.go
Normal file
83
lib/challenge/proofofwork/proofofwork.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package proofofwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
chall "github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/web"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
chall.Register("fast", &Impl{Algorithm: "fast"})
|
||||||
|
chall.Register("slow", &Impl{Algorithm: "slow"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Impl struct {
|
||||||
|
Algorithm string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Setup(mux *http.ServeMux) {
|
||||||
|
/* no implementation required */
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error) {
|
||||||
|
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't render page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return component, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error {
|
||||||
|
nonceStr := r.FormValue("nonce")
|
||||||
|
if nonceStr == "" {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, err := strconv.Atoi(nonceStr)
|
||||||
|
if err != nil {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTimeStr := r.FormValue("elapsedTime")
|
||||||
|
if elapsedTimeStr == "" {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w elapsedTime", chall.ErrMissingField))
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: elapsedTime: %w", chall.ErrInvalidFormat, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
response := r.FormValue("response")
|
||||||
|
if response == "" {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
|
||||||
|
}
|
||||||
|
|
||||||
|
calcString := fmt.Sprintf("%s%d", challenge, nonce)
|
||||||
|
calculated := internal.SHA256sum(calcString)
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the leading zeroes
|
||||||
|
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
|
||||||
|
}
|
||||||
|
|
||||||
|
lg.Debug("challenge took", "elapsedTime", elapsedTime)
|
||||||
|
chall.TimeTaken.WithLabelValues(i.Algorithm).Observe(elapsedTime)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
136
lib/challenge/proofofwork/proofofwork_test.go
Normal file
136
lib/challenge/proofofwork/proofofwork_test.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package proofofwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mkRequest(t *testing.T, values map[string]string) *http.Request {
|
||||||
|
t.Helper()
|
||||||
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic(t *testing.T) {
|
||||||
|
i := &Impl{Algorithm: "fast"}
|
||||||
|
bot := &policy.Bot{
|
||||||
|
Challenge: &config.ChallengeRules{
|
||||||
|
Algorithm: "fast",
|
||||||
|
Difficulty: 0,
|
||||||
|
ReportAs: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const challengeStr = "hunter"
|
||||||
|
const response = "2652bdba8fb4d2ab39ef28d8534d7694c557a4ae146c1e9237bd8d950280500e"
|
||||||
|
|
||||||
|
for _, cs := range []struct {
|
||||||
|
name string
|
||||||
|
req *http.Request
|
||||||
|
err error
|
||||||
|
challengeStr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "allgood",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: nil,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-params",
|
||||||
|
req: mkRequest(t, map[string]string{}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-nonce",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-elapsedTime",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-response",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong-nonce-format",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "taco",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrInvalidFormat,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong-elapsedTime-format",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "taco",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrInvalidFormat,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-response",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrFailed,
|
||||||
|
challengeStr: "Tacos are tasty",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(cs.name, func(t *testing.T) {
|
||||||
|
lg := slog.With()
|
||||||
|
|
||||||
|
if _, err := i.Issue(cs.req, lg, bot, cs.challengeStr, nil); err != nil {
|
||||||
|
t.Errorf("can't issue challenge: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Validate(cs.req, lg, bot, cs.challengeStr); !errors.Is(err, cs.err) {
|
||||||
|
t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package lib
|
|||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/web"
|
"github.com/TecharoHQ/anubis/web"
|
||||||
"github.com/TecharoHQ/anubis/xess"
|
"github.com/TecharoHQ/anubis/xess"
|
||||||
@@ -65,6 +67,17 @@ func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedC
|
|||||||
}(fin)
|
}(fin)
|
||||||
|
|
||||||
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
||||||
|
var validationErrs []error
|
||||||
|
|
||||||
|
for _, b := range anubisPolicy.Bots {
|
||||||
|
if _, ok := challenge.Get(b.Challenge.Algorithm); !ok {
|
||||||
|
validationErrs = append(validationErrs, fmt.Errorf("%w %s", policy.ErrChallengeRuleHasWrongAlgorithm, b.Challenge.Algorithm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validationErrs) != 0 {
|
||||||
|
return nil, fmt.Errorf("can't do final validation of Anubis config: %w", errors.Join(validationErrs...))
|
||||||
|
}
|
||||||
|
|
||||||
return anubisPolicy, err
|
return anubisPolicy, err
|
||||||
}
|
}
|
||||||
@@ -132,12 +145,21 @@ func New(opts Options) (*Server, error) {
|
|||||||
}), "GET")
|
}), "GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
|
|
||||||
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
||||||
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
||||||
registerWithPrefix(anubis.APIPrefix+"test-error", http.HandlerFunc(result.TestError), "GET")
|
|
||||||
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
||||||
|
|
||||||
|
//goland:noinspection GoBoolExpressions
|
||||||
|
if anubis.Version == "devel" {
|
||||||
|
// make-challenge is only used in tests. Only enable while version is devel
|
||||||
|
registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, implKind := range challenge.Methods() {
|
||||||
|
impl, _ := challenge.Get(implKind)
|
||||||
|
impl.Setup(mux)
|
||||||
|
}
|
||||||
|
|
||||||
result.mux = mux
|
result.mux = mux
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
51
lib/config_test.go
Normal file
51
lib/config_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInvalidChallengeMethod(t *testing.T) {
|
||||||
|
if _, err := LoadPoliciesOrDefault("testdata/invalid-challenge-method.yaml", 4); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
||||||
|
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadConfigs(t *testing.T) {
|
||||||
|
finfos, err := os.ReadDir("policy/config/testdata/bad")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range finfos {
|
||||||
|
st := st
|
||||||
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
|
if _, err := LoadPoliciesOrDefault(filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoodConfigs(t *testing.T) {
|
||||||
|
finfos, err := os.ReadDir("policy/config/testdata/good")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range finfos {
|
||||||
|
st := st
|
||||||
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
|
if _, err := LoadPoliciesOrDefault(filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
77
lib/http.go
77
lib/http.go
@@ -1,24 +1,44 @@
|
|||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/web"
|
"github.com/TecharoHQ/anubis/web"
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) ClearCookie(w http.ResponseWriter) {
|
func (s *Server) SetCookie(w http.ResponseWriter, name, value, path string) {
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: s.cookieName,
|
Name: name,
|
||||||
Value: "",
|
Value: value,
|
||||||
Expires: time.Now().Add(-1 * time.Hour),
|
Expires: time.Now().Add(s.opts.CookieExpiration),
|
||||||
MaxAge: -1,
|
SameSite: http.SameSiteLaxMode,
|
||||||
SameSite: http.SameSiteLaxMode,
|
Domain: s.opts.CookieDomain,
|
||||||
Domain: s.opts.CookieDomain,
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
|
Path: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ClearCookie(w http.ResponseWriter, name, path string) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
Expires: time.Now().Add(-1 * time.Minute),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
|
Domain: s.opts.CookieDomain,
|
||||||
|
Path: path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +58,10 @@ func (t UnixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
return t.Transport.RoundTrip(req)
|
return t.Transport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func randomChance(n int) bool {
|
||||||
|
return rand.Intn(n) == 0
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
|
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
|
||||||
if returnHTTPStatusOnly {
|
if returnHTTPStatusOnly {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
@@ -47,7 +71,13 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
|||||||
|
|
||||||
lg := internal.GetRequestLogger(r)
|
lg := internal.GetRequestLogger(r)
|
||||||
|
|
||||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
|
||||||
|
lg.Error("client was given a challenge but does not in fact support gzip compression")
|
||||||
|
s.respondWithError(w, r, "Client Error: Please ensure your browser is up to date and try again later.")
|
||||||
|
}
|
||||||
|
|
||||||
|
challengesIssued.WithLabelValues("embedded").Add(1)
|
||||||
|
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||||
|
|
||||||
var ogTags map[string]string = nil
|
var ogTags map[string]string = nil
|
||||||
if s.opts.OGPassthrough {
|
if s.opts.OGPassthrough {
|
||||||
@@ -58,17 +88,31 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: anubis.TestCookieName,
|
||||||
|
Value: challengeStr,
|
||||||
|
Expires: time.Now().Add(30 * time.Minute),
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
|
||||||
|
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||||
|
if !ok {
|
||||||
|
lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm)
|
||||||
|
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
component, err := impl.Issue(r, lg, rule, challengeStr, ogTags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
|
lg.Error("[unexpected] render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
|
||||||
s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"")
|
s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := internal.NoStoreCache(templ.Handler(
|
handler := internal.GzipMiddleware(1, internal.NoStoreCache(templ.Handler(
|
||||||
component,
|
component,
|
||||||
templ.WithStatus(s.opts.Policy.StatusCodes.Challenge),
|
templ.WithStatus(s.opts.Policy.StatusCodes.Challenge),
|
||||||
))
|
)))
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +157,15 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
web.Base("You are not a bot!", web.StaticHappy()),
|
web.Base("You are not a bot!", web.StaticHappy()),
|
||||||
).ServeHTTP(w, r)
|
).ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
|
requestsProxied.WithLabelValues(r.Host).Inc()
|
||||||
s.next.ServeHTTP(w, r)
|
s.next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) signJWT(claims jwt.MapClaims) (string, error) {
|
||||||
|
claims["iat"] = time.Now().Unix()
|
||||||
|
claims["nbf"] = time.Now().Add(-1 * time.Minute).Unix()
|
||||||
|
claims["exp"] = time.Now().Add(s.opts.CookieExpiration).Unix()
|
||||||
|
|
||||||
|
return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.priv)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func TestClearCookie(t *testing.T) {
|
|||||||
srv := spawnAnubis(t, Options{})
|
srv := spawnAnubis(t, Options{})
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
srv.ClearCookie(rw)
|
srv.ClearCookie(rw, srv.cookieName, "/")
|
||||||
|
|
||||||
resp := rw.Result()
|
resp := rw.Result()
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ func TestClearCookieWithDomain(t *testing.T) {
|
|||||||
srv := spawnAnubis(t, Options{CookieDomain: "techaro.lol"})
|
srv := spawnAnubis(t, Options{CookieDomain: "techaro.lol"})
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
srv.ClearCookie(rw)
|
srv.ClearCookie(rw, srv.cookieName, "/")
|
||||||
|
|
||||||
resp := rw.Result()
|
resp := rw.Result()
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Bot struct {
|
|||||||
Challenge *config.ChallengeRules
|
Challenge *config.ChallengeRules
|
||||||
Name string
|
Name string
|
||||||
Action config.Rule
|
Action config.Rule
|
||||||
|
Weight *config.Weight
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Bot) Hash() string {
|
func (b Bot) Hash() string {
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ func (cl CheckerList) Hash() string {
|
|||||||
return internal.SHA256sum(sb.String())
|
return internal.SHA256sum(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type staticHashChecker struct {
|
||||||
|
hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (staticHashChecker) Check(r *http.Request) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticHashChecker) Hash() string { return s.hash }
|
||||||
|
|
||||||
|
func NewStaticHashChecker(hashable string) Checker {
|
||||||
|
return staticHashChecker{hash: internal.SHA256sum(hashable)}
|
||||||
|
}
|
||||||
|
|
||||||
type RemoteAddrChecker struct {
|
type RemoteAddrChecker struct {
|
||||||
ranger cidranger.Ranger
|
ranger cidranger.Ranger
|
||||||
hash string
|
hash string
|
||||||
@@ -62,7 +76,10 @@ func NewRemoteAddrChecker(cidrs []string) (Checker, error) {
|
|||||||
return nil, fmt.Errorf("%w: range %s not parsing: %w", ErrMisconfiguration, cidr, err)
|
return nil, fmt.Errorf("%w: range %s not parsing: %w", ErrMisconfiguration, cidr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ranger.Insert(cidranger.NewBasicRangerEntry(*rng))
|
err = ranger.Insert(cidranger.NewBasicRangerEntry(*rng))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: error inserting ip range: %w", ErrMisconfiguration, err)
|
||||||
|
}
|
||||||
fmt.Fprintln(&sb, cidr)
|
fmt.Fprintln(&sb, cidr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CheckResult struct {
|
type CheckResult struct {
|
||||||
Name string
|
Name string
|
||||||
Rule config.Rule
|
Rule config.Rule
|
||||||
|
Weight int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr CheckResult) LogValue() slog.Value {
|
func (cr CheckResult) LogValue() slog.Value {
|
||||||
return slog.GroupValue(
|
return slog.GroupValue(
|
||||||
slog.String("name", cr.Name),
|
slog.String("name", cr.Name),
|
||||||
slog.String("rule", string(cr.Rule)))
|
slog.String("rule", string(cr.Rule)),
|
||||||
|
slog.Int("weight", cr.Weight),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,26 +39,22 @@ const (
|
|||||||
RuleAllow Rule = "ALLOW"
|
RuleAllow Rule = "ALLOW"
|
||||||
RuleDeny Rule = "DENY"
|
RuleDeny Rule = "DENY"
|
||||||
RuleChallenge Rule = "CHALLENGE"
|
RuleChallenge Rule = "CHALLENGE"
|
||||||
|
RuleWeigh Rule = "WEIGH"
|
||||||
RuleBenchmark Rule = "DEBUG_BENCHMARK"
|
RuleBenchmark Rule = "DEBUG_BENCHMARK"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Algorithm string
|
const DefaultAlgorithm = "fast"
|
||||||
|
|
||||||
const (
|
|
||||||
AlgorithmUnknown Algorithm = ""
|
|
||||||
AlgorithmFast Algorithm = "fast"
|
|
||||||
AlgorithmSlow Algorithm = "slow"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BotConfig struct {
|
type BotConfig struct {
|
||||||
UserAgentRegex *string `json:"user_agent_regex"`
|
UserAgentRegex *string `json:"user_agent_regex,omitempty"`
|
||||||
PathRegex *string `json:"path_regex"`
|
PathRegex *string `json:"path_regex,omitempty"`
|
||||||
HeadersRegex map[string]string `json:"headers_regex"`
|
HeadersRegex map[string]string `json:"headers_regex,omitempty"`
|
||||||
Expression *ExpressionOrList `json:"expression"`
|
Expression *ExpressionOrList `json:"expression,omitempty"`
|
||||||
Challenge *ChallengeRules `json:"challenge,omitempty"`
|
Challenge *ChallengeRules `json:"challenge,omitempty"`
|
||||||
|
Weight *Weight `json:"weight,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Action Rule `json:"action"`
|
Action Rule `json:"action"`
|
||||||
RemoteAddr []string `json:"remote_addresses"`
|
RemoteAddr []string `json:"remote_addresses,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b BotConfig) Zero() bool {
|
func (b BotConfig) Zero() bool {
|
||||||
@@ -79,7 +75,7 @@ func (b BotConfig) Zero() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b BotConfig) Valid() error {
|
func (b *BotConfig) Valid() error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
if b.Name == "" {
|
if b.Name == "" {
|
||||||
@@ -150,7 +146,7 @@ func (b BotConfig) Valid() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch b.Action {
|
switch b.Action {
|
||||||
case RuleAllow, RuleBenchmark, RuleChallenge, RuleDeny:
|
case RuleAllow, RuleBenchmark, RuleChallenge, RuleDeny, RuleWeigh:
|
||||||
// okay
|
// okay
|
||||||
default:
|
default:
|
||||||
errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
|
errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
|
||||||
@@ -162,6 +158,10 @@ func (b BotConfig) Valid() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.Action == RuleWeigh && b.Weight == nil {
|
||||||
|
b.Weight = &Weight{Adjust: 5}
|
||||||
|
}
|
||||||
|
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...))
|
return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...))
|
||||||
}
|
}
|
||||||
@@ -170,15 +170,14 @@ func (b BotConfig) Valid() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChallengeRules struct {
|
type ChallengeRules struct {
|
||||||
Algorithm Algorithm `json:"algorithm"`
|
Algorithm string `json:"algorithm"`
|
||||||
Difficulty int `json:"difficulty"`
|
Difficulty int `json:"difficulty"`
|
||||||
ReportAs int `json:"report_as"`
|
ReportAs int `json:"report_as"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
|
ErrChallengeDifficultyTooLow = errors.New("config.Bot.ChallengeRules: difficulty is too low (must be >= 1)")
|
||||||
ErrChallengeDifficultyTooLow = errors.New("config.Bot.ChallengeRules: difficulty is too low (must be >= 1)")
|
ErrChallengeDifficultyTooHigh = errors.New("config.Bot.ChallengeRules: difficulty is too high (must be <= 64)")
|
||||||
ErrChallengeDifficultyTooHigh = errors.New("config.Bot.ChallengeRules: difficulty is too high (must be <= 64)")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cr ChallengeRules) Valid() error {
|
func (cr ChallengeRules) Valid() error {
|
||||||
@@ -192,13 +191,6 @@ func (cr ChallengeRules) Valid() error {
|
|||||||
errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty))
|
errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch cr.Algorithm {
|
|
||||||
case AlgorithmFast, AlgorithmSlow, AlgorithmUnknown:
|
|
||||||
// do nothing, it's all good
|
|
||||||
default:
|
|
||||||
errs = append(errs, fmt.Errorf("%w: %q", ErrChallengeRuleHasWrongAlgorithm, cr.Algorithm))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...))
|
return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...))
|
||||||
}
|
}
|
||||||
@@ -224,7 +216,7 @@ func (is *ImportStatement) open() (fs.File, error) {
|
|||||||
func (is *ImportStatement) load() error {
|
func (is *ImportStatement) load() error {
|
||||||
fin, err := is.open()
|
fin, err := is.open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't open %s: %w", is.Import, err)
|
return fmt.Errorf("%w: %s: %w", ErrInvalidImportStatement, is.Import, err)
|
||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -130,20 +130,6 @@ func TestBotValid(t *testing.T) {
|
|||||||
},
|
},
|
||||||
err: ErrChallengeDifficultyTooHigh,
|
err: ErrChallengeDifficultyTooHigh,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "challenge wrong algorithm",
|
|
||||||
bot: BotConfig{
|
|
||||||
Name: "mozilla-ua",
|
|
||||||
Action: RuleChallenge,
|
|
||||||
PathRegex: p("Mozilla"),
|
|
||||||
Challenge: &ChallengeRules{
|
|
||||||
Difficulty: 420,
|
|
||||||
ReportAs: 4,
|
|
||||||
Algorithm: "high quality rips",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: ErrChallengeRuleHasWrongAlgorithm,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "invalid cidr range",
|
name: "invalid cidr range",
|
||||||
bot: BotConfig{
|
bot: BotConfig{
|
||||||
@@ -182,6 +168,25 @@ func TestBotValid(t *testing.T) {
|
|||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "weight rule without weight",
|
||||||
|
bot: BotConfig{
|
||||||
|
Name: "weight-adjust-if-mozilla",
|
||||||
|
Action: RuleWeigh,
|
||||||
|
UserAgentRegex: p("Mozilla"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "weight rule with weight adjust",
|
||||||
|
bot: BotConfig{
|
||||||
|
Name: "weight-adjust-if-mozilla",
|
||||||
|
Action: RuleWeigh,
|
||||||
|
UserAgentRegex: p("Mozilla"),
|
||||||
|
Weight: &Weight{
|
||||||
|
Adjust: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cs := range tests {
|
for _, cs := range tests {
|
||||||
@@ -251,6 +256,7 @@ func TestImportStatement(t *testing.T) {
|
|||||||
"bots",
|
"bots",
|
||||||
"common",
|
"common",
|
||||||
"crawlers",
|
"crawlers",
|
||||||
|
"meta",
|
||||||
} {
|
} {
|
||||||
if err := fs.WalkDir(data.BotPolicies, folderName, func(path string, d fs.DirEntry, err error) error {
|
if err := fs.WalkDir(data.BotPolicies, folderName, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -259,6 +265,9 @@ func TestImportStatement(t *testing.T) {
|
|||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if d.Name() == "README.md" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
tests = append(tests, testCase{
|
tests = append(tests, testCase{
|
||||||
name: "(data)/" + path,
|
name: "(data)/" + path,
|
||||||
@@ -357,7 +366,7 @@ func TestBotConfigZero(t *testing.T) {
|
|||||||
b.Challenge = &ChallengeRules{
|
b.Challenge = &ChallengeRules{
|
||||||
Difficulty: 4,
|
Difficulty: 4,
|
||||||
ReportAs: 4,
|
ReportAs: 4,
|
||||||
Algorithm: AlgorithmFast,
|
Algorithm: DefaultAlgorithm,
|
||||||
}
|
}
|
||||||
if b.Zero() {
|
if b.Zero() {
|
||||||
t.Error("BotConfig with challenge rules is zero value")
|
t.Error("BotConfig with challenge rules is zero value")
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ var (
|
|||||||
|
|
||||||
type ExpressionOrList struct {
|
type ExpressionOrList struct {
|
||||||
Expression string `json:"-"`
|
Expression string `json:"-"`
|
||||||
All []string `json:"all"`
|
All []string `json:"all,omitempty"`
|
||||||
Any []string `json:"any"`
|
Any []string `json:"any,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (eol ExpressionOrList) Equal(rhs *ExpressionOrList) bool {
|
func (eol ExpressionOrList) Equal(rhs *ExpressionOrList) bool {
|
||||||
@@ -54,6 +54,9 @@ func (eol *ExpressionOrList) UnmarshalJSON(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (eol *ExpressionOrList) Valid() error {
|
func (eol *ExpressionOrList) Valid() error {
|
||||||
|
if eol.Expression == "" && len(eol.All) == 0 && len(eol.Any) == 0 {
|
||||||
|
return ErrExpressionEmpty
|
||||||
|
}
|
||||||
if len(eol.All) != 0 && len(eol.Any) != 0 {
|
if len(eol.All) != 0 && len(eol.Any) != 0 {
|
||||||
return ErrExpressionCantHaveBoth
|
return ErrExpressionCantHaveBoth
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ func TestExpressionOrListUnmarshal(t *testing.T) {
|
|||||||
}`,
|
}`,
|
||||||
validErr: ErrExpressionCantHaveBoth,
|
validErr: ErrExpressionCantHaveBoth,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "expression-empty",
|
||||||
|
inp: `{
|
||||||
|
"any": []
|
||||||
|
}`,
|
||||||
|
validErr: ErrExpressionEmpty,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var eol ExpressionOrList
|
var eol ExpressionOrList
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"bots": [
|
"bots": [
|
||||||
{
|
{
|
||||||
"import": "(data)/bots/ai-robots-txt.yaml",
|
"import": "(data)/bots/ai-catchall.yaml",
|
||||||
"name": "generic-browser",
|
"name": "generic-browser",
|
||||||
"user_agent_regex": "Mozilla|Opera\n",
|
"user_agent_regex": "Mozilla|Opera\n",
|
||||||
"action": "CHALLENGE"
|
"action": "CHALLENGE"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
bots:
|
bots:
|
||||||
- import: (data)/bots/ai-robots-txt.yaml
|
- import: (data)/bots/ai-catchall.yaml
|
||||||
name: generic-browser
|
name: generic-browser
|
||||||
user_agent_regex: >
|
user_agent_regex: >
|
||||||
Mozilla|Opera
|
Mozilla|Opera
|
||||||
|
|||||||
8
lib/policy/config/testdata/good/entropy.yaml
vendored
Normal file
8
lib/policy/config/testdata/good/entropy.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
bots:
|
||||||
|
- name: total-randomness
|
||||||
|
action: ALLOW
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- '"Accept" in headers'
|
||||||
|
- headers["Accept"].contains("text/html")
|
||||||
|
- randInt(1) == 0
|
||||||
6
lib/policy/config/testdata/good/simple-weight.yaml
vendored
Normal file
6
lib/policy/config/testdata/good/simple-weight.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
bots:
|
||||||
|
- name: simple-weight-adjust
|
||||||
|
action: WEIGH
|
||||||
|
user_agent_regex: Mozilla
|
||||||
|
weight:
|
||||||
|
adjust: 5
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user