mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-07 09:18:18 +00:00
Compare commits
5 Commits
Xe/checks-
...
Xe/load-av
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009df6299a | ||
|
|
efa1b6d8a8 | ||
|
|
c0922ba1c3 | ||
|
|
1f286d6eb7 | ||
|
|
9b67cedff2 |
@@ -2,12 +2,10 @@
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
|
||||
{
|
||||
"name": "Dev",
|
||||
"dockerComposeFile": [
|
||||
"./docker-compose.yaml"
|
||||
],
|
||||
"dockerComposeFile": ["./docker-compose.yaml"],
|
||||
"service": "workspace",
|
||||
"workspaceFolder": "/workspace/anubis",
|
||||
"postStartCommand": "bash ./.devcontainer/poststart.sh",
|
||||
"postStartCommand": "npm ci && go mod download",
|
||||
"features": {
|
||||
"ghcr.io/xe/devcontainer-features/ko:1.1.0": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
@@ -21,9 +19,8 @@
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
pwd
|
||||
|
||||
npm ci &
|
||||
go mod download &
|
||||
go install ./utils/cmd/... &
|
||||
|
||||
wait
|
||||
2
.github/actions/spelling/allow.txt
vendored
2
.github/actions/spelling/allow.txt
vendored
@@ -3,5 +3,3 @@ https
|
||||
ssh
|
||||
ubuntu
|
||||
workarounds
|
||||
rjack
|
||||
msgbox
|
||||
1
.github/actions/spelling/excludes.txt
vendored
1
.github/actions/spelling/excludes.txt
vendored
@@ -92,4 +92,3 @@ ignore$
|
||||
robots.txt
|
||||
^lib/localization/locales/.*\.json$
|
||||
^lib/localization/.*_test.go$
|
||||
^test/.*$
|
||||
|
||||
26
.github/actions/spelling/expect.txt
vendored
26
.github/actions/spelling/expect.txt
vendored
@@ -1,10 +1,12 @@
|
||||
acs
|
||||
aeacus
|
||||
Aibrew
|
||||
alrest
|
||||
amazonbot
|
||||
anthro
|
||||
anubis
|
||||
anubistest
|
||||
apk
|
||||
Applebot
|
||||
archlinux
|
||||
asnc
|
||||
@@ -23,7 +25,6 @@ bitrate
|
||||
Bluesky
|
||||
blueskybot
|
||||
boi
|
||||
Bokm
|
||||
botnet
|
||||
botstopper
|
||||
BPort
|
||||
@@ -60,6 +61,7 @@ connnection
|
||||
containerbuild
|
||||
coreutils
|
||||
Cotoyogi
|
||||
CRDs
|
||||
Cromite
|
||||
crt
|
||||
Cscript
|
||||
@@ -130,7 +132,6 @@ Hashcash
|
||||
hashrate
|
||||
headermap
|
||||
healthcheck
|
||||
healthz
|
||||
hec
|
||||
hmc
|
||||
hostable
|
||||
@@ -146,10 +147,8 @@ Imagesift
|
||||
imgproxy
|
||||
impressum
|
||||
inp
|
||||
internets
|
||||
IPTo
|
||||
iptoasn
|
||||
isp
|
||||
iss
|
||||
isset
|
||||
ivh
|
||||
@@ -161,6 +160,7 @@ jshelter
|
||||
JWTs
|
||||
kagi
|
||||
kagibot
|
||||
keikaku
|
||||
Keyfunc
|
||||
keypair
|
||||
KHTML
|
||||
@@ -174,13 +174,13 @@ lgbt
|
||||
licend
|
||||
licstart
|
||||
lightpanda
|
||||
limsa
|
||||
LIMSA
|
||||
Linting
|
||||
linuxbrew
|
||||
LLU
|
||||
loadbalancer
|
||||
lol
|
||||
lominsa
|
||||
LOMINSA
|
||||
maintainership
|
||||
malware
|
||||
mcr
|
||||
@@ -188,7 +188,7 @@ memes
|
||||
metarefresh
|
||||
metrix
|
||||
mimi
|
||||
Minfilia
|
||||
minica
|
||||
mistralai
|
||||
Mojeek
|
||||
mojeekbot
|
||||
@@ -223,7 +223,6 @@ pipefail
|
||||
pki
|
||||
podkova
|
||||
podman
|
||||
poststart
|
||||
prebaked
|
||||
privkey
|
||||
promauto
|
||||
@@ -243,19 +242,21 @@ redhat
|
||||
redir
|
||||
redirectscheme
|
||||
refactors
|
||||
relayd
|
||||
reputational
|
||||
reqmeta
|
||||
risc
|
||||
ruleset
|
||||
runlevels
|
||||
RUnlock
|
||||
runtimedir
|
||||
runtimedirectory
|
||||
sas
|
||||
sasl
|
||||
searchbot
|
||||
searx
|
||||
sebest
|
||||
secretplans
|
||||
selfsigned
|
||||
Semrush
|
||||
Seo
|
||||
setsebool
|
||||
@@ -298,8 +299,10 @@ uberspace
|
||||
Unbreak
|
||||
unbreakdocker
|
||||
unifiedjs
|
||||
unixhttpd
|
||||
unmarshal
|
||||
unparseable
|
||||
uuidgen
|
||||
uvx
|
||||
UXP
|
||||
valkey
|
||||
@@ -308,8 +311,6 @@ Velen
|
||||
vendored
|
||||
vhosts
|
||||
videotest
|
||||
VKE
|
||||
Vultr
|
||||
waitloop
|
||||
weblate
|
||||
webmaster
|
||||
@@ -319,11 +320,11 @@ websites
|
||||
Webzio
|
||||
wildbase
|
||||
withthothmock
|
||||
wolfbeast
|
||||
wordpress
|
||||
Workaround
|
||||
workdir
|
||||
wpbot
|
||||
xcaddy
|
||||
Xeact
|
||||
xeiaso
|
||||
xeserv
|
||||
@@ -340,7 +341,6 @@ yeet
|
||||
yeetfile
|
||||
yourdomain
|
||||
yoursite
|
||||
yyz
|
||||
Zenos
|
||||
zizmor
|
||||
zombocom
|
||||
|
||||
4
.github/actions/spelling/patterns.txt
vendored
4
.github/actions/spelling/patterns.txt
vendored
@@ -132,7 +132,3 @@ go install(?:\s+[a-z]+\.[-@\w/.]+)+
|
||||
# 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()=./%]*
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# data url
|
||||
\bdata:[-a-zA-Z=;:/0-9+]*,\S*
|
||||
11
.github/workflows/docs-deploy.yml
vendored
11
.github/workflows/docs-deploy.yml
vendored
@@ -36,9 +36,6 @@ jobs:
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/techarohq/anubis/docs
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
main
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
@@ -52,15 +49,15 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
- name: Apply k8s manifests to aeacus
|
||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
args: apply -k docs/manifest
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
- name: Apply k8s manifests to aeacus
|
||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
|
||||
7
.github/workflows/docs-test.yml
vendored
7
.github/workflows/docs-test.yml
vendored
@@ -2,7 +2,7 @@ name: Docs test build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: [ "main" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -24,10 +24,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/techarohq/anubis/docs
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
main
|
||||
images: ghcr.io/${{ github.repository }}/docs
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
|
||||
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
@@ -82,7 +82,7 @@ jobs:
|
||||
run: npm run test
|
||||
|
||||
- name: Lint with staticcheck
|
||||
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0
|
||||
uses: dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6 # v1.3.1
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
|
||||
117
.github/workflows/package-builds-stable.yml
vendored
117
.github/workflows/package-builds-stable.yml
vendored
@@ -1,9 +1,8 @@
|
||||
name: Package builds (stable)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# release:
|
||||
# types: [published]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -14,67 +13,67 @@ jobs:
|
||||
#runs-on: alrest-techarohq
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: build essential
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential
|
||||
- name: build essential
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential
|
||||
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
/home/linuxbrew/.linuxbrew/bin
|
||||
/home/linuxbrew/.linuxbrew/etc
|
||||
/home/linuxbrew/.linuxbrew/include
|
||||
/home/linuxbrew/.linuxbrew/lib
|
||||
/home/linuxbrew/.linuxbrew/opt
|
||||
/home/linuxbrew/.linuxbrew/sbin
|
||||
/home/linuxbrew/.linuxbrew/share
|
||||
/home/linuxbrew/.linuxbrew/var
|
||||
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-homebrew-cellar-
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
/home/linuxbrew/.linuxbrew/bin
|
||||
/home/linuxbrew/.linuxbrew/etc
|
||||
/home/linuxbrew/.linuxbrew/include
|
||||
/home/linuxbrew/.linuxbrew/lib
|
||||
/home/linuxbrew/.linuxbrew/opt
|
||||
/home/linuxbrew/.linuxbrew/sbin
|
||||
/home/linuxbrew/.linuxbrew/share
|
||||
/home/linuxbrew/.linuxbrew/var
|
||||
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-homebrew-cellar-
|
||||
|
||||
- name: Install Brew dependencies
|
||||
run: |
|
||||
brew bundle
|
||||
- name: Install Brew dependencies
|
||||
run: |
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-golang-
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-golang-
|
||||
|
||||
- name: install node deps
|
||||
run: |
|
||||
npm ci
|
||||
- name: install node deps
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Build Packages
|
||||
run: |
|
||||
go tool yeet
|
||||
- name: Build Packages
|
||||
run: |
|
||||
go tool yeet
|
||||
|
||||
- name: Upload released artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.TOKEN }}
|
||||
RELEASE_VERSION: ${{github.event.release.tag_name}}
|
||||
shell: bash
|
||||
run: |
|
||||
RELEASE="${RELEASE_VERSION}"
|
||||
cd var
|
||||
for file in *; do
|
||||
gh release upload $RELEASE $file
|
||||
done
|
||||
- name: Upload released artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.TOKEN }}
|
||||
RELEASE_VERSION: ${{github.event.release.tag_name}}
|
||||
shell: bash
|
||||
run: |
|
||||
RELEASE="${RELEASE_VERSION}"
|
||||
cd var
|
||||
for file in *; do
|
||||
gh release upload $RELEASE $file
|
||||
done
|
||||
|
||||
58
.github/workflows/smoke-tests.yml
vendored
58
.github/workflows/smoke-tests.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Smoke tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
strategy:
|
||||
matrix:
|
||||
test:
|
||||
- git-clone
|
||||
- git-push
|
||||
- healthcheck
|
||||
- i18n
|
||||
- palemoon/amd64
|
||||
#- palemoon/i386
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
||||
|
||||
- name: Install utils
|
||||
run: |
|
||||
go install ./utils/cmd/...
|
||||
|
||||
- name: Run test
|
||||
run: |
|
||||
cd test/${{ matrix.test }}
|
||||
backoff-retry --try-count 10 ./test.sh
|
||||
|
||||
- name: Sanitize artifact name
|
||||
if: always()
|
||||
run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: test/${{ matrix.test }}/var
|
||||
8
.github/workflows/ssh-ci.yml
vendored
8
.github/workflows/ssh-ci.yml
vendored
@@ -25,19 +25,13 @@ jobs:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install CI target SSH key
|
||||
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
|
||||
with:
|
||||
key: ${{ secrets.CI_SSH_KEY }}
|
||||
name: id_rsa
|
||||
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
|
||||
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Run CI
|
||||
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
|
||||
run: bash test/ssh-ci/rigging.sh ${{ matrix.host }}
|
||||
env:
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
uses: github/codeql-action/upload-sarif@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -5,7 +5,6 @@
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
@@ -48,13 +48,6 @@ Anubis is brought to you by sponsors and donors like:
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://fabulous.systems/">
|
||||
<img
|
||||
src="./docs/static/img/sponsors/fabulous-systems.webp"
|
||||
alt="Cat eyes over the word Emma in a serif font"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -30,16 +30,14 @@ import (
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/thoth"
|
||||
libanubis "github.com/TecharoHQ/anubis/lib"
|
||||
"github.com/TecharoHQ/anubis/lib/checker/headerexists"
|
||||
botPolicy "github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/facebookgo/flagenv"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -99,7 +97,7 @@ func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
||||
}
|
||||
|
||||
func doHealthCheck() error {
|
||||
resp, err := http.Get("http://localhost" + *metricsBind + "/healthz")
|
||||
resp, err := http.Get("http://localhost" + *metricsBind + anubis.BasePrefix + "/metrics")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch metrics: %w", err)
|
||||
}
|
||||
@@ -243,15 +241,6 @@ func main() {
|
||||
}
|
||||
|
||||
internal.InitSlog(*slogLevel)
|
||||
internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||
|
||||
if *healthcheck {
|
||||
log.Println("running healthcheck")
|
||||
if err := doHealthCheck(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if *extractResources != "" {
|
||||
if err := extractEmbedFS(data.BotPolicies, ".", *extractResources); err != nil {
|
||||
@@ -264,17 +253,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// install signal handler
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
if *metricsBind != "" {
|
||||
wg.Add(1)
|
||||
go metricsServer(ctx, wg.Done)
|
||||
}
|
||||
|
||||
var rp http.Handler
|
||||
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
|
||||
if strings.TrimSpace(*target) != "" {
|
||||
@@ -289,6 +267,8 @@ func main() {
|
||||
log.Fatalf("you can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN at the same time")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Thoth configuration
|
||||
switch {
|
||||
case *thothURL != "" && *thothToken == "":
|
||||
@@ -324,7 +304,7 @@ func main() {
|
||||
if *debugBenchmarkJS {
|
||||
policy.Bots = []botPolicy.Bot{{
|
||||
Name: "",
|
||||
Rules: headerexists.New("User-Agent"),
|
||||
Rules: botPolicy.NewHeaderExistsChecker("User-Agent"),
|
||||
Action: config.RuleBenchmark,
|
||||
}}
|
||||
}
|
||||
@@ -418,12 +398,21 @@ func main() {
|
||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
}
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
// install signal handler
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if *metricsBind != "" {
|
||||
wg.Add(1)
|
||||
go metricsServer(ctx, wg.Done)
|
||||
}
|
||||
|
||||
var h http.Handler
|
||||
h = s
|
||||
h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
|
||||
h = internal.XForwardedForToXRealIP(h)
|
||||
h = internal.XForwardedForUpdate(*xffStripPrivate, h)
|
||||
h = internal.JA4H(h)
|
||||
|
||||
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
||||
@@ -452,8 +441,6 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
internal.SetHealth("anubis", healthv1.HealthCheckResponse_SERVING)
|
||||
|
||||
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -464,30 +451,20 @@ func metricsServer(ctx context.Context, done func()) {
|
||||
defer done()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
st, ok := internal.GetHealth("anubis")
|
||||
if !ok {
|
||||
slog.Error("health service anubis does not exist, file a bug")
|
||||
}
|
||||
|
||||
switch st {
|
||||
case healthv1.HealthCheckResponse_NOT_SERVING:
|
||||
http.Error(w, "NOT OK", http.StatusInternalServerError)
|
||||
return
|
||||
case healthv1.HealthCheckResponse_SERVING:
|
||||
fmt.Fprintln(w, "OK")
|
||||
return
|
||||
default:
|
||||
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
|
||||
return
|
||||
}
|
||||
})
|
||||
mux.Handle(anubis.BasePrefix+"/metrics", promhttp.Handler())
|
||||
|
||||
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
|
||||
slog.Debug("listening for metrics", "url", metricsUrl)
|
||||
|
||||
if *healthcheck {
|
||||
log.Println("running healthcheck")
|
||||
if err := doHealthCheck(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker/expression"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
@@ -38,11 +37,11 @@ type RobotsRule struct {
|
||||
}
|
||||
|
||||
type AnubisRule struct {
|
||||
Expression *expression.Config `yaml:"expression,omitempty" json:"expression,omitempty"`
|
||||
Challenge *config.ChallengeRules `yaml:"challenge,omitempty" json:"challenge,omitempty"`
|
||||
Weight *config.Weight `yaml:"weight,omitempty" json:"weight,omitempty"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Action string `yaml:"action" json:"action"`
|
||||
Expression *config.ExpressionOrList `yaml:"expression,omitempty" json:"expression,omitempty"`
|
||||
Challenge *config.ChallengeRules `yaml:"challenge,omitempty" json:"challenge,omitempty"`
|
||||
Weight *config.Weight `yaml:"weight,omitempty" json:"weight,omitempty"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Action string `yaml:"action" json:"action"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -225,11 +224,11 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
}
|
||||
|
||||
if userAgent == "*" {
|
||||
rule.Expression = &expression.Config{
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{"true"}, // Always applies
|
||||
}
|
||||
} else {
|
||||
rule.Expression = &expression.Config{
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)},
|
||||
}
|
||||
}
|
||||
@@ -250,11 +249,11 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
rule.Name = fmt.Sprintf("%s-global-restriction-%d", *policyName, ruleCounter)
|
||||
rule.Action = "WEIGH"
|
||||
rule.Weight = &config.Weight{Adjust: 20} // Increase difficulty significantly
|
||||
rule.Expression = &expression.Config{
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{"true"}, // Always applies
|
||||
}
|
||||
} else {
|
||||
rule.Expression = &expression.Config{
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)},
|
||||
}
|
||||
}
|
||||
@@ -286,7 +285,7 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
pathCondition := buildPathCondition(disallow)
|
||||
conditions = append(conditions, pathCondition)
|
||||
|
||||
rule.Expression = &expression.Config{
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: conditions,
|
||||
}
|
||||
|
||||
|
||||
@@ -74,13 +74,13 @@ bots:
|
||||
weight:
|
||||
adjust: 10
|
||||
|
||||
# ## System load based checks.
|
||||
# # If the system is under high load, add weight.
|
||||
# - name: high-load-average
|
||||
# action: WEIGH
|
||||
# expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
|
||||
# weight:
|
||||
# adjust: 20
|
||||
## System load based checks.
|
||||
# If the system is under high load, add weight.
|
||||
- name: high-load-average
|
||||
action: WEIGH
|
||||
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: 20
|
||||
|
||||
## If your backend service is running on the same operating system as Anubis,
|
||||
## you can uncomment this rule to make the challenge easier when the system is
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
- name: uptime-robot
|
||||
user_agent_regex: UptimeRobot
|
||||
action: ALLOW
|
||||
# https://api.uptimerobot.com/meta/ips
|
||||
remote_addresses: [
|
||||
"3.12.251.153/32",
|
||||
"3.20.63.178/32",
|
||||
"3.77.67.4/32",
|
||||
"3.79.134.69/32",
|
||||
"3.105.133.239/32",
|
||||
"3.105.190.221/32",
|
||||
"3.133.226.214/32",
|
||||
"3.149.57.90/32",
|
||||
"3.212.128.62/32",
|
||||
"5.161.61.238/32",
|
||||
"5.161.73.160/32",
|
||||
"5.161.75.7/32",
|
||||
"5.161.113.195/32",
|
||||
"5.161.117.52/32",
|
||||
"5.161.177.47/32",
|
||||
"5.161.194.92/32",
|
||||
"5.161.215.244/32",
|
||||
"5.223.43.32/32",
|
||||
"5.223.53.147/32",
|
||||
"5.223.57.22/32",
|
||||
"18.116.205.62/32",
|
||||
"18.180.208.214/32",
|
||||
"18.192.166.72/32",
|
||||
"18.193.252.127/32",
|
||||
"24.144.78.39/32",
|
||||
"24.144.78.185/32",
|
||||
"34.198.201.66/32",
|
||||
"45.55.123.175/32",
|
||||
"45.55.127.146/32",
|
||||
"49.13.24.81/32",
|
||||
"49.13.130.29/32",
|
||||
"49.13.134.145/32",
|
||||
"49.13.164.148/32",
|
||||
"49.13.167.123/32",
|
||||
"52.15.147.27/32",
|
||||
"52.22.236.30/32",
|
||||
"52.28.162.93/32",
|
||||
"52.59.43.236/32",
|
||||
"52.87.72.16/32",
|
||||
"54.64.67.106/32",
|
||||
"54.79.28.129/32",
|
||||
"54.87.112.51/32",
|
||||
"54.167.223.174/32",
|
||||
"54.249.170.27/32",
|
||||
"63.178.84.147/32",
|
||||
"64.225.81.248/32",
|
||||
"64.225.82.147/32",
|
||||
"69.162.124.227/32",
|
||||
"69.162.124.235/32",
|
||||
"69.162.124.238/32",
|
||||
"78.46.190.63/32",
|
||||
"78.46.215.1/32",
|
||||
"78.47.98.55/32",
|
||||
"78.47.173.76/32",
|
||||
"88.99.80.227/32",
|
||||
"91.99.101.207/32",
|
||||
"128.140.41.193/32",
|
||||
"128.140.106.114/32",
|
||||
"129.212.132.140/32",
|
||||
"134.199.240.137/32",
|
||||
"138.197.53.117/32",
|
||||
"138.197.53.138/32",
|
||||
"138.197.54.143/32",
|
||||
"138.197.54.247/32",
|
||||
"138.197.63.92/32",
|
||||
"139.59.50.44/32",
|
||||
"142.132.180.39/32",
|
||||
"143.198.249.237/32",
|
||||
"143.198.250.89/32",
|
||||
"143.244.196.21/32",
|
||||
"143.244.196.211/32",
|
||||
"143.244.221.177/32",
|
||||
"144.126.251.21/32",
|
||||
"146.190.9.187/32",
|
||||
"152.42.149.135/32",
|
||||
"157.90.155.240/32",
|
||||
"157.90.156.63/32",
|
||||
"159.69.158.189/32",
|
||||
"159.223.243.219/32",
|
||||
"161.35.247.201/32",
|
||||
"167.99.18.52/32",
|
||||
"167.235.143.113/32",
|
||||
"168.119.53.160/32",
|
||||
"168.119.96.239/32",
|
||||
"168.119.123.75/32",
|
||||
"170.64.250.64/32",
|
||||
"170.64.250.132/32",
|
||||
"170.64.250.235/32",
|
||||
"178.156.181.172/32",
|
||||
"178.156.184.20/32",
|
||||
"178.156.185.127/32",
|
||||
"178.156.185.231/32",
|
||||
"178.156.187.238/32",
|
||||
"178.156.189.113/32",
|
||||
"178.156.189.249/32",
|
||||
"188.166.201.79/32",
|
||||
"206.189.241.133/32",
|
||||
"209.38.49.1/32",
|
||||
"209.38.49.206/32",
|
||||
"209.38.49.226/32",
|
||||
"209.38.51.43/32",
|
||||
"209.38.53.7/32",
|
||||
"209.38.124.252/32",
|
||||
"216.144.248.18/31",
|
||||
"216.144.248.21/32",
|
||||
"216.144.248.22/31",
|
||||
"216.144.248.24/30",
|
||||
"216.144.248.28/31",
|
||||
"216.144.248.30/32",
|
||||
"216.245.221.83/32",
|
||||
"2400:6180:10:200::56a0:b000/128",
|
||||
"2400:6180:10:200::56a0:c000/128",
|
||||
"2400:6180:10:200::56a0:e000/128",
|
||||
"2400:6180:100:d0::94b6:4001/128",
|
||||
"2400:6180:100:d0::94b6:5001/128",
|
||||
"2400:6180:100:d0::94b6:7001/128",
|
||||
"2406:da14:94d:8601:9d0d:7754:bedf:e4f5/128",
|
||||
"2406:da14:94d:8601:b325:ff58:2bba:7934/128",
|
||||
"2406:da14:94d:8601:db4b:c5ac:2cbe:9a79/128",
|
||||
"2406:da1c:9c8:dc02:7ae1:f2ea:ab91:2fde/128",
|
||||
"2406:da1c:9c8:dc02:7db9:f38b:7b9f:402e/128",
|
||||
"2406:da1c:9c8:dc02:82b2:f0fd:ee96:579/128",
|
||||
"2600:1f16:775:3a00:ac3:c5eb:7081:942e/128",
|
||||
"2600:1f16:775:3a00:37bf:6026:e54a:f03a/128",
|
||||
"2600:1f16:775:3a00:3f24:5bb0:95d7:5a6b/128",
|
||||
"2600:1f16:775:3a00:8c2c:2ba6:778f:5be5/128",
|
||||
"2600:1f16:775:3a00:91ac:3120:ff38:92b5/128",
|
||||
"2600:1f16:775:3a00:dbbe:36b0:3c45:da32/128",
|
||||
"2600:1f18:179:f900:71:af9a:ade7:d772/128",
|
||||
"2600:1f18:179:f900:2406:9399:4ae6:c5d3/128",
|
||||
"2600:1f18:179:f900:4696:7729:7bb3:f52f/128",
|
||||
"2600:1f18:179:f900:4b7d:d1cc:2d10:211/128",
|
||||
"2600:1f18:179:f900:5c68:91b6:5d75:5d7/128",
|
||||
"2600:1f18:179:f900:e8dd:eed1:a6c:183b/128",
|
||||
"2604:a880:800:14:0:1:68ba:d000/128",
|
||||
"2604:a880:800:14:0:1:68ba:e000/128",
|
||||
"2604:a880:800:14:0:1:68bb:0/128",
|
||||
"2604:a880:800:14:0:1:68bb:1000/128",
|
||||
"2604:a880:800:14:0:1:68bb:3000/128",
|
||||
"2604:a880:800:14:0:1:68bb:4000/128",
|
||||
"2604:a880:800:14:0:1:68bb:5000/128",
|
||||
"2604:a880:800:14:0:1:68bb:6000/128",
|
||||
"2604:a880:800:14:0:1:68bb:7000/128",
|
||||
"2604:a880:800:14:0:1:68bb:a000/128",
|
||||
"2604:a880:800:14:0:1:68bb:b000/128",
|
||||
"2604:a880:800:14:0:1:68bb:c000/128",
|
||||
"2604:a880:800:14:0:1:68bb:d000/128",
|
||||
"2604:a880:800:14:0:1:68bb:e000/128",
|
||||
"2604:a880:800:14:0:1:68bb:f000/128",
|
||||
"2607:ff68:107::4/128",
|
||||
"2607:ff68:107::14/128",
|
||||
"2607:ff68:107::33/128",
|
||||
"2607:ff68:107::48/127",
|
||||
"2607:ff68:107::50/125",
|
||||
"2607:ff68:107::58/127",
|
||||
"2607:ff68:107::60/128",
|
||||
"2a01:4f8:c0c:83fa::1/128",
|
||||
"2a01:4f8:c17:42e4::1/128",
|
||||
"2a01:4f8:c2c:9fc6::1/128",
|
||||
"2a01:4f8:c2c:beae::1/128",
|
||||
"2a01:4f8:1c1a:3d53::1/128",
|
||||
"2a01:4f8:1c1b:4ef4::1/128",
|
||||
"2a01:4f8:1c1b:5b5a::1/128",
|
||||
"2a01:4f8:1c1b:7ecc::1/128",
|
||||
"2a01:4f8:1c1c:11aa::1/128",
|
||||
"2a01:4f8:1c1c:5353::1/128",
|
||||
"2a01:4f8:1c1c:7240::1/128",
|
||||
"2a01:4f8:1c1c:a98a::1/128",
|
||||
"2a01:4f8:c012:c60e::1/128",
|
||||
"2a01:4f8:c013:c18::1/128",
|
||||
"2a01:4f8:c013:34c0::1/128",
|
||||
"2a01:4f8:c013:3b0f::1/128",
|
||||
"2a01:4f8:c013:3c52::1/128",
|
||||
"2a01:4f8:c013:3c53::1/128",
|
||||
"2a01:4f8:c013:3c54::1/128",
|
||||
"2a01:4f8:c013:3c55::1/128",
|
||||
"2a01:4f8:c013:3c56::1/128",
|
||||
"2a01:4ff:f0:bfd::1/128",
|
||||
"2a01:4ff:f0:2219::1/128",
|
||||
"2a01:4ff:f0:3e03::1/128",
|
||||
"2a01:4ff:f0:5f80::1/128",
|
||||
"2a01:4ff:f0:7fad::1/128",
|
||||
"2a01:4ff:f0:9c5f::1/128",
|
||||
"2a01:4ff:f0:b2f2::1/128",
|
||||
"2a01:4ff:f0:b6f1::1/128",
|
||||
"2a01:4ff:f0:d283::1/128",
|
||||
"2a01:4ff:f0:d3cd::1/128",
|
||||
"2a01:4ff:f0:e516::1/128",
|
||||
"2a01:4ff:f0:e9cf::1/128",
|
||||
"2a01:4ff:f0:eccb::1/128",
|
||||
"2a01:4ff:f0:efd1::1/128",
|
||||
"2a01:4ff:f0:fdc7::1/128",
|
||||
"2a01:4ff:2f0:193c::1/128",
|
||||
"2a01:4ff:2f0:27de::1/128",
|
||||
"2a01:4ff:2f0:3b3a::1/128",
|
||||
"2a03:b0c0:2:f0::bd91:f001/128",
|
||||
"2a03:b0c0:2:f0::bd92:1/128",
|
||||
"2a03:b0c0:2:f0::bd92:1001/128",
|
||||
"2a03:b0c0:2:f0::bd92:2001/128",
|
||||
"2a03:b0c0:2:f0::bd92:4001/128",
|
||||
"2a03:b0c0:2:f0::bd92:5001/128",
|
||||
"2a03:b0c0:2:f0::bd92:6001/128",
|
||||
"2a03:b0c0:2:f0::bd92:7001/128",
|
||||
"2a03:b0c0:2:f0::bd92:8001/128",
|
||||
"2a03:b0c0:2:f0::bd92:9001/128",
|
||||
"2a03:b0c0:2:f0::bd92:a001/128",
|
||||
"2a03:b0c0:2:f0::bd92:b001/128",
|
||||
"2a03:b0c0:2:f0::bd92:c001/128",
|
||||
"2a03:b0c0:2:f0::bd92:e001/128",
|
||||
"2a03:b0c0:2:f0::bd92:f001/128",
|
||||
"2a05:d014:1815:3400:6d:9235:c1c0:96ad/128",
|
||||
"2a05:d014:1815:3400:654f:bd37:724c:212b/128",
|
||||
"2a05:d014:1815:3400:90b4:4ef9:5631:b170/128",
|
||||
"2a05:d014:1815:3400:9779:d8e9:100a:9642/128",
|
||||
"2a05:d014:1815:3400:af29:e95e:64ff:df81/128",
|
||||
"2a05:d014:1815:3400:c7d6:f7f3:6cc1:30d1/128",
|
||||
"2a05:d014:1815:3400:d784:e5dd:8e0:67cb/128",
|
||||
]
|
||||
@@ -19,3 +19,5 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Kubernetes manifests
|
||||
/manifest
|
||||
@@ -5,7 +5,6 @@ COPY . .
|
||||
|
||||
RUN npm ci && npm run build
|
||||
|
||||
FROM ghcr.io/xe/nginx-micro
|
||||
COPY --from=build /app/build /www
|
||||
COPY ./manifest/cfg/nginx/nginx.conf /conf
|
||||
FROM docker.io/library/nginx:alpine
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
||||
@@ -20,9 +20,9 @@ If you rely on Anubis to keep your website safe, please consider sponsoring the
|
||||
|
||||
I am waiting to hear back from NLNet on if Anubis was selected for funding or not. Let's hope it is!
|
||||
|
||||
## Deprecation warning: `DIFFICULTY`
|
||||
## Deprecation warning: `DEFAULT_DIFFICULTY`
|
||||
|
||||
Anubis v1.20.0 is the last version to support the `DIFFICULTY` flag in the exact way it currently does. In future versions, this will be ineffectual and you should use the [custom threshold system](/docs/admin/configuration/thresholds) instead.
|
||||
Anubis v1.20.0 is the last version to support the `DEFAULT_DIFFICULTY` flag in the exact way it currently does. In future versions, this will be ineffectual and you should use the [custom threshold system](/docs/admin/configuration/thresholds) instead.
|
||||
|
||||
If this becomes an imposition in practice, this will be reverted.
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
slug: incident/TI-20250709-0001
|
||||
title: "TI-20250709-0001: IPv4 traffic failures for Techaro services"
|
||||
authors: [xe]
|
||||
tags: [incident]
|
||||
image: ./window-portal.jpg
|
||||
---
|
||||
|
||||

|
||||
|
||||
Techaro services were down for IPv4 traffic on July 9th, 2025. This blogpost is a report of what happened, what actions were taken to resolve the situation, and what actions are being done in the near future to prevent this problem. Enjoy this incident report!
|
||||
|
||||
{/* truncate */}
|
||||
|
||||
:::note
|
||||
|
||||
In other companies, this kind of documentation would be kept internal. At Techaro, we believe that you deserve radical candor and the truth. As such, we are proving our lofty words with actions by publishing details about how things go wrong publicly.
|
||||
|
||||
Everything past this point follows my standard incident root cause meeting template.
|
||||
|
||||
:::
|
||||
|
||||
This incident report will focus on the services affected, timeline of what happened at which stage of the incident, where we got lucky, the root cause analysis, and what action items are being planned or taken to prevent this from happening in the future.
|
||||
|
||||
## Timeline
|
||||
|
||||
All events take place on July 9th, 2025.
|
||||
|
||||
| Time (UTC) | Description |
|
||||
| :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 12:32 | Uptime Kuma reports that another unrelated website on the same cluster was timing out. |
|
||||
| 12:33 | Uptime Kuma reports that Thoth's production endpoint is failing gRPC health checks. |
|
||||
| 12:35 | Investigation begins, [announcement made on Xe's Bluesky](https://bsky.app/profile/xeiaso.net/post/3ltjtdczpwc2x) due to the impact including their personal blog. |
|
||||
| 12:39 | `nginx-ingress` logs on the production cluster show IPv6 traffic but an abrupt cutoff in IPv4 traffic around 12:32 UTC. Ticket is opened with the hosting provider. |
|
||||
| 12:41 | IPv4 traffic resumes long enough for Uptime Kuma to report uptime, but then immediately fails again. |
|
||||
| 12:46 | IPv4 traffic resumes long enough for Uptime Kuma to report uptime, but then immediately fails again. (repeat instances of this have been scrubbed, but it happened about every 5-10 minutes) |
|
||||
| 12:48 | First reply from the hosting provider. |
|
||||
| 12:57 | Reply to hosting provider, ask to reboot the load balancer. |
|
||||
| 13:00 | Incident responder because busy due to a meeting under the belief that the downtime was out of their control and that uptime monitoring software would let them know if it came back up. |
|
||||
| 13:20 | Incident responder ended meeting and went back to monitoring downtime and preparing this document. |
|
||||
| 13:34 | IPv4 traffic starts to show up in the `ingress-nginx` logs. |
|
||||
| 13:35 | All services start to report healthy. Incident status changes to monitoring. |
|
||||
| 13:48 | Incident closed. |
|
||||
| 14:07 | Incident re-opened. Issues seem to be manifesting as BGP issues in the upstream provider. |
|
||||
| 14:10 | IPv4 traffic resumes and then stops. |
|
||||
| 14:18 | IPv4 traffic resumes again. Incident status changes to monitoring. |
|
||||
| 14:40 | Incident closed. |
|
||||
|
||||
## Services affected
|
||||
|
||||
| Service name | User impact |
|
||||
| :-------------------------------------------------- | :----------------- |
|
||||
| [Anubis Docs](https://anubis.techaro.lol) (IPv4) | Connection timeout |
|
||||
| [Anubis Docs](https://anubis.techaro.lol) (IPv6) | None |
|
||||
| [Thoth](/docs/admin/thoth/) (IPv4) | Connection timeout |
|
||||
| [Thoth](/docs/admin/thoth/) (IPv6) | None |
|
||||
| Other websites colocated on the same cluster (IPv4) | Connection timeout |
|
||||
| Other websites colocated on the same cluster (IPv6) | None |
|
||||
|
||||
## Root cause analysis
|
||||
|
||||
In simplify server management, Techaro runs a [Kubernetes](https://kubernetes.io/) cluster on [Vultr VKE](https://www.vultr.com/kubernetes/) (Vultr Kubernetes Engine). When you do this, Vultr needs to provision a [load balancer](https://docs.vultr.com/how-to-use-a-vultr-load-balancer-with-vke) to bridge the gap between the outside world and the Kubernetes world, kinda like this:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Overall architecture
|
||||
---
|
||||
|
||||
flowchart LR
|
||||
UT(User Traffic)
|
||||
subgraph Provider Infrastructure
|
||||
LB[Load Balancer]
|
||||
end
|
||||
subgraph Kubernetes
|
||||
IN(ingress-nginx)
|
||||
TH(Thoth)
|
||||
AN(Anubis Docs)
|
||||
OS(Other sites)
|
||||
|
||||
IN --> TH
|
||||
IN --> AN
|
||||
IN --> OS
|
||||
end
|
||||
|
||||
UT --> LB --> IN
|
||||
```
|
||||
|
||||
Techaro controls everything inside the Kubernetes side of that diagram. Anything else is out of our control. That load balancer is routed to the public internet via [Border Gateway Protocol (BGP)](https://en.wikipedia.org/wiki/Border_Gateway_Protocol).
|
||||
|
||||
If there is an interruption with the BGP sessions in the upstream provider, this can manifest as things either not working or inconsistently working. This is made more difficult by the fact that the IPv4 and IPv6 internets are technically separate networks. With this in mind, it's very possible to have IPv4 traffic fail but not IPv6 traffic.
|
||||
|
||||
The root cause is that the hosting provider we use for production services had flapping IPv4 BGP sessions in its Toronto region. When this happens all we can do is open a ticket and wait for it to come back up.
|
||||
|
||||
## Where we got lucky
|
||||
|
||||
The Uptime Kuma instance that caught this incident runs on an IPv4-only network. If it was dual stack, this would not have been caught as quickly.
|
||||
|
||||
The `ingress-nginx` logs print IP addresses of remote clients to the log feed. If this was not the case, it would be much more difficult to find this error.
|
||||
|
||||
## Action items
|
||||
|
||||
- A single instance of downtime like this is not enough reason to move providers. Moving providers because of this is thus out of scope.
|
||||
- Techaro needs a status page hosted on a different cloud provider than is used for the production cluster (`TecharoHQ/TODO#6`).
|
||||
- Health checks for IPv4 and IPv6 traffic need to be created (`TecharoHQ/TODO#7`).
|
||||
- Remove the requirement for [Anubis to pass Thoth health checks before it can start if Thoth is enabled](https://github.com/TecharoHQ/anubis/pull/794).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB |
@@ -1,369 +0,0 @@
|
||||
---
|
||||
slug: release/v1.21.1
|
||||
title: Anubis v1.21.1 is now available!
|
||||
authors: [xe]
|
||||
tags: [release]
|
||||
image: anubis-i18n.webp
|
||||
---
|
||||
|
||||

|
||||
|
||||
Hey all!
|
||||
|
||||
Recently we released [Anubis v1.21.1: Minfilia Warde (Echo 1)](https://github.com/TecharoHQ/anubis/releases/tag/v1.21.1). This is a fairly meaty release and like [last time](../2025-06-27-release-1.20.0/index.mdx) this blogpost will tell you what you need to know before you update. Kick back, get some popcorn and let's dig into this!
|
||||
|
||||
{/* truncate */}
|
||||
|
||||
In this release, Anubis becomes internationalized, gains the ability to use system load as input to issuing challenges, finally fixes the "invalid response" after "success" bug, and more! Please read these notes before upgrading as the changes are big enough that administrators should take action to ensure that the upgrade goes smoothly.
|
||||
|
||||
This release is brought to you by [FreeCAD](https://www.freecad.org/), an open-source computer aided design tool that lets you design things for the real world.
|
||||
|
||||
## What's in this release?
|
||||
|
||||
The biggest change is that the ["invalid response" after "success" bug](https://github.com/TecharoHQ/anubis/issues/564) is now finally fixed for good by totally rewriting how [Anubis' challenge issuance flow works](#challenge-flow-v2).
|
||||
|
||||
This release gives Anubis the following features:
|
||||
|
||||
- [Internationalization support](#internationalization), allowing Anubis to render its messages in the human language you speak.
|
||||
- Anubis now supports the [`missingHeader`](#missingHeader-function) function to assert the absence of headers in requests.
|
||||
- Anubis now has the ability to [store data persistently on the server](#persistent-data-storage).
|
||||
- Anubis can use [the system load average](#load-average-checks) as a factor to determine if it needs to filter traffic or not.
|
||||
- Add `COOKIE_SECURE` option to set the cookie [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies)
|
||||
- Sets cookie defaults to use [SameSite: None](https://web.dev/articles/samesite-cookies-explained)
|
||||
- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
|
||||
- Add `/healthz` metrics route for use in platform-based health checks.
|
||||
- Start exposing JA4H fingerprints for later use in CEL expressions.
|
||||
|
||||
And this release also fixes the following bugs:
|
||||
|
||||
- [Challenge issuance has been totally rewritten](#challenge-flow-v2) to finally squash the infamous ["invalid response" after "success" bug](https://github.com/TecharoHQ/anubis/issues/564) for good.
|
||||
- In order to reduce confusion, the "Success" interstitial that shows up when you pass a proof of work challenge has been removed.
|
||||
- Don't block Anubis starting up if [Thoth](/docs/admin/thoth/) health checks fail.
|
||||
- The "Try again" button on the error page has been fixed. Previously it meant "try the solution again" instead of "try the challenge again".
|
||||
- In certain cases, a user could be stuck with a test cookie that is invalid, locking them out of the service for up to half an hour. This has been fixed with better validation of this case and clearing the cookie.
|
||||
- "Proof of work" has been removed from the branding due to some users having extremely negative connotations with it.
|
||||
|
||||
We try to avoid introducing breaking changes as much as possible, but these are the changes that may be relevant for you as an administrator:
|
||||
|
||||
- The [challenge format](#challenge-format-change) has been changed in order to account for [the new challenge issuance flow](#challenge-flow-v2).
|
||||
- The [systemd service `RuntimeDirectory` has been changed](#breaking-change-systemd-runtimedirectory-change).
|
||||
|
||||
### Sponsoring the project
|
||||
|
||||
If you rely on Anubis to keep your website safe, please consider sponsoring the project on [GitHub Sponsors](https://github.com/sponsors/Xe) or [Patreon](https://patreon.com/cadey). Funding helps pay hosting bills and offset the time spent on making this project the best it can be. Every little bit helps and when enough money is raised, [I can make Anubis my full-time job](https://github.com/TecharoHQ/anubis/discussions/278).
|
||||
|
||||
Once this pie chart is at 100%, I can start to reduce my hours at my day job as most of my needs will be met (pre-tax):
|
||||
|
||||
```mermaid
|
||||
pie title Funding update
|
||||
"GitHub Sponsors" : 29
|
||||
"Patreon" : 14
|
||||
"Remaining" : 56
|
||||
```
|
||||
|
||||
I am waiting to hear back from NLNet on if Anubis was selected for funding or not. Let's hope it is!
|
||||
|
||||
## New features
|
||||
|
||||
### Internationalization
|
||||
|
||||
Anubis now supports localized responses. Locales can be added in [lib/localization/locales/](https://github.com/TecharoHQ/anubis/tree/main/lib/localization/locales). This release includes support for the following languages:
|
||||
|
||||
- [Brazilian Portugese](https://github.com/TecharoHQ/anubis/pull/726)
|
||||
- [Chinese (Simplified)](https://github.com/TecharoHQ/anubis/pull/774)
|
||||
- [Chinese (Traditional)](https://github.com/TecharoHQ/anubis/pull/759)
|
||||
- [Czech](https://github.com/TecharoHQ/anubis/pull/849)
|
||||
- English
|
||||
- [Estonian](https://github.com/TecharoHQ/anubis/pull/783)
|
||||
- [Filipino](https://github.com/TecharoHQ/anubis/pull/775)
|
||||
- [Finnish](https://github.com/TecharoHQ/anubis/pull/863)
|
||||
- [French](https://github.com/TecharoHQ/anubis/pull/716)
|
||||
- [German](https://github.com/TecharoHQ/anubis/pull/741)
|
||||
- [Japanese](https://github.com/TecharoHQ/anubis/pull/772)
|
||||
- [Icelandic](https://github.com/TecharoHQ/anubis/pull/780)
|
||||
- [Italian](https://github.com/TecharoHQ/anubis/pull/778)
|
||||
- [Norwegian](https://github.com/TecharoHQ/anubis/pull/855)
|
||||
- [Russian](https://github.com/TecharoHQ/anubis/pull/882)
|
||||
- [Spanish](https://github.com/TecharoHQ/anubis/pull/716)
|
||||
- [Turkish](https://github.com/TecharoHQ/anubis/pull/751)
|
||||
|
||||
If facts or local regulations demand, you can set Anubis default language with the `FORCED_LANGUAGE` environment variable or the `--forced-language` command line argument:
|
||||
|
||||
```sh
|
||||
FORCED_LANGUAGE=de
|
||||
```
|
||||
|
||||
## Big ticket bug fixes
|
||||
|
||||
These issues affect every user of Anubis. Administrators should upgrade Anubis as soon as possible to mitigate them.
|
||||
|
||||
### Fix event loop thrashing when solving a proof of work challenge
|
||||
|
||||
Anubis has a progress bar so that users can have something moving while it works. This gives users more confidence that something is happening and that the website is not being malicious with CPU usage. However, the way it was implemented way back in [#87](https://github.com/TecharoHQ/anubis/pull/87) had a subtle bug:
|
||||
|
||||
```js
|
||||
if (
|
||||
(nonce > oldNonce) | 1023 && // we've wrapped past 1024
|
||||
(nonce >> 10) % threads === threadId // and it's our turn
|
||||
) {
|
||||
postMessage(nonce);
|
||||
}
|
||||
```
|
||||
|
||||
The logic here looks fine but is subtly wrong as was reported in [#877](https://github.com/TecharoHQ/anubis/issues/877) by the main Pale Moon developer.
|
||||
|
||||
For context, `nonce` is a counter that increments by the worker count every loop. This is intended to spread the load between CPU cores as such:
|
||||
|
||||
| Iteration | Worker ID | Nonce |
|
||||
| :-------- | :-------- | :---- |
|
||||
| 1 | 0 | 0 |
|
||||
| 1 | 1 | 1 |
|
||||
| 2 | 0 | 2 |
|
||||
| 2 | 1 | 3 |
|
||||
|
||||
And so on. This makes the proof of work challenge as fast as it can possibly be so that Anubis quickly goes away and you can enjoy the service it is protecting.
|
||||
|
||||
The incorrect part of this is the boolean logic, specifically the part with the bitwise or `|`. I think the intent was to use a logical or (`||`), but this had the effect of making the `postMessage` handler fire on every iteration. The intent of this snippet (as the comment clearly indicates) is to make sure that the main event loop is only updated with the worker status every 1024 iterations per worker. This had the opposite effect, causing a lot of messages to be sent from workers to the parent JavaScript context.
|
||||
|
||||
This is bad for the event loop.
|
||||
|
||||
Instead, I have ripped out that statement and replaced it with a much simpler increment only counter that fires every 1024 iterations. Additionally, only the first thread communicates back to the parent process. This does mean that in theory the other workers could be ahead of the first thread (posting a message out of a worker has a nonzero cost), but in practice I don't think this will be as much of an issue as the current behaviour is.
|
||||
|
||||
The root cause of the stack exhaustion is likely the pressure caused by all of the postMessage futures piling up. Maybe the larger stack size in 64 bit environments is causing this to be fine there, maybe it's some combination of newer hardware in 64 bit systems making this not be as much of a problem due to it being able to handle events fast enough to keep up with the pressure.
|
||||
|
||||
Either way, thanks much to [@wolfbeast](https://github.com/wolfbeast) and the Pale Moon community for finding this. This will make Anubis faster for everyone!
|
||||
|
||||
### Fix potential memory leak when discovering a solution
|
||||
|
||||
In some cases, the parallel solution finder in Anubis could cause all of the worker promises to leak due to the fact the promises were being improperly terminated. A recursion bomb happens in the following scenario:
|
||||
|
||||
1. A worker sends a message indicating it found a solution to the proof of work challenge.
|
||||
2. The `onmessage` handler for that worker calls `terminate()`
|
||||
3. Inside `terminate()`, the parent process loops through all other workers and calls `w.terminate()` on them.
|
||||
4. It's possible that terminating a worker could lead to the `onerror` event handler.
|
||||
5. This would create a recursive loop of `onmessage` -> `terminate` -> `onerror` -> `terminate` -> `onerror` and so on.
|
||||
|
||||
This infinite recursion quickly consumes all available stack space, but this has never been noticed in development because all of my computers have at least 64Gi of ram provisioned to them under the axiom paying for more ram is cheaper than paying in my time spent having to work around not having enough ram. Additionally, ia32 has a smaller base stack size, which means that they will run into this issue much sooner than users on other CPU architectures will.
|
||||
|
||||
The fix adds a boolean `settled` flag to prevent termination from running more than once.
|
||||
|
||||
## Expressions features
|
||||
|
||||
Anubis v1.21.1 adds additional [expressions](/docs/admin/configuration/expressions) features so that you can make your request matching even more granular.
|
||||
|
||||
### `missingHeader` function
|
||||
|
||||
Anubis [expressions](/docs/admin/configuration/expressions) have [a few functions exposed](/docs/admin/configuration/expressions/#functions-exposed-to-anubis-expressions). Anubis v1.21.1 adds the `missingHeader` function, allowing you to assert the _absence_ of a header in requests.
|
||||
|
||||
Let's say you're getting a lot of requests from clients that are pretending to be Google Chrome. Google Chrome sends a few signals to web servers, the main one of them is the [`Sec-Ch-Ua`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-CH-UA). Sec-CH-UA is part of Google's [User Agent Client Hints](https://wicg.github.io/ua-client-hints/#sec-ch-ua) proposal, but it being present is a sign that the client is more likely Google Chrome than not. With the `missingHeader` function, you can write a rule to [add weight](/docs/admin/policies/#request-weight) to requests without `Sec-Ch-Ua` that claim to be Google Chrome.
|
||||
|
||||
```yaml
|
||||
# Adds weight clients that claim to be Google Chrome without setting Sec-Ch-Ua
|
||||
- name: old-chrome
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: 10
|
||||
expression:
|
||||
all:
|
||||
- userAgent.matches("Chrome/[1-9][0-9]?\\.0\\.0\\.0")
|
||||
- missingHeader(headers, "Sec-Ch-Ua")
|
||||
```
|
||||
|
||||
When combined with [weight thresholds](/docs/admin/configuration/thresholds), this allows you to make requests that don't match the signature of Google Chrome more suspicious, which will make them have a more difficult challenge.
|
||||
|
||||
### Load average checks
|
||||
|
||||
Anubis can dynamically take action [based on the system load average](/docs/admin/configuration/expressions/#using-the-system-load-average), allowing you to write rules like this:
|
||||
|
||||
```yaml
|
||||
## System load based checks.
|
||||
# If the system is under high load for the last minute, add weight.
|
||||
- name: high-load-average
|
||||
action: WEIGH
|
||||
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: 20
|
||||
|
||||
# If it is not for the last 15 minutes, remove weight.
|
||||
- name: low-load-average
|
||||
action: WEIGH
|
||||
expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: -10
|
||||
```
|
||||
|
||||
Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example:
|
||||
|
||||
| Kind | Core count | Load threshold |
|
||||
| --------: | :--------- | :------------- |
|
||||
| high load | 4 | `8.0` |
|
||||
| low load | 4 | `2.0` |
|
||||
| high load | 16 | `32.0` |
|
||||
| low load | 16 | `8` |
|
||||
|
||||
Also keep in mind that this does not account for other kinds of latency like I/O latency or downstream API response latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
|
||||
|
||||
:::note
|
||||
|
||||
This does not work if you are using Kubernetes.
|
||||
|
||||
:::
|
||||
|
||||
When combined with [weight thresholds](/docs/admin/configuration/thresholds), this allows you to make incoming sessions "back off" while the server is under high load.
|
||||
|
||||
## Challenge flow v2
|
||||
|
||||
The main goal of Anubis is to weigh the risks of incoming requests in order to protect upstream resources against abusive clients like badly written scrapers. In order to separate "good" clients (like users wanting to learn from a website's content) from "bad" clients, Anubis issues [challenges](/docs/category/challenges).
|
||||
|
||||
Previously the Anubis challenge flow looked like this:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Old Anubis challenge flow
|
||||
---
|
||||
flowchart LR
|
||||
user(User Browser)
|
||||
subgraph Anubis
|
||||
mIC{Challenge?}
|
||||
ic(Issue Challenge)
|
||||
rp(Proxy to service)
|
||||
mIC -->|User needs a challenge| ic
|
||||
mIC -->|User does not need a challenge| rp
|
||||
end
|
||||
target(Target Service)
|
||||
rp --> target
|
||||
user --> mIC
|
||||
ic -->|Pass a challenge| user
|
||||
target -->|Site data| users
|
||||
```
|
||||
|
||||
In order to issue a challenge, Anubis generated a challenge string based on request metadata that we assumed wouldn't drastically change between requests, including but not limited to:
|
||||
|
||||
- The client's User-Agent string.
|
||||
- The client [`Accept-Language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language) value.
|
||||
- The client's IP address.
|
||||
|
||||
Anubis also didn't store any information about challenges so that it can remain lightweight and handle the onslaught of requests from scrapers. The assumption was that the challenge string function was idempotent per client across time. What actually ended up happening was something like this:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Anubis challenge string idempotency
|
||||
---
|
||||
sequenceDiagram
|
||||
User->>+Anubis: GET /wiki/some-page
|
||||
Anubis->>+Make Challenge: Generate a challenge string
|
||||
Make Challenge->>-Anubis: Challenge string: taco salad
|
||||
Anubis->>-User: HTTP 401 solve a challenge
|
||||
User->>+Anubis: GET internal-api/pass-challenge
|
||||
Anubis->>+Make Challenge: Generate a challenge string
|
||||
Make Challenge->>-Anubis: Challenge string: burrito bar
|
||||
Anubis->>+User: Error: invalid response
|
||||
```
|
||||
|
||||
Various attempts were made to fix this. All of these ended up failing. Many difficulties were discovered including but not limited to:
|
||||
|
||||
- Removing `Accept-Language` from consideration because [Chrome randomizes the contents of `Accept-Language` to reduce fingerprinting](https://github.com/explainers-by-googlers/reduce-accept-language), a behaviour which [causes a lot of confusion](https://www.reddit.com/r/chrome/comments/nhpnez/google_chrome_is_randomly_switching_languages_on/) for users with multiple system languages selected.
|
||||
- [IPv6 privacy extensions](https://www.internetsociety.org/resources/deploy360/2014/privacy-extensions-for-ipv6-slaac/) mean that each request could be coming from a different IP address (at least one legitimate user in the wild has been observed to have a different IP address per TCP session across an entire `/48`).
|
||||
- Some [US mobile phone carriers make it too easy for your IP address to drastically change](https://news.ycombinator.com/item?id=32038215) without user input.
|
||||
- [Happy eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs) means that some requests can come in over IPv4 and some requests can come in over IPv6.
|
||||
- To make things worse, you can't even assert that users are from the same [BGP autonomous system](<https://en.wikipedia.org/wiki/Autonomous_system_(Internet)>) because some users could have ISPs that are IPv4 only, forcing them to use a different IP address space to get IPv6 internet access. This sounds like it's rare enough, but I personally have to do this even though I pay for 8 gigabit fiber from my ISP and only get IPv4 service from them.
|
||||
|
||||
Amusingly enough, the only part of this that has survived is the assertion that a user hasn't changed their `User-Agent` string. Maybe [that one guy that sets his Chrome version to `150`](https://github.com/TecharoHQ/anubis/issues/239) would have issues, but so far I've not seen any evidence that a client randomly changing their user agent between challenge issuance and solving can possibly be legitimate.
|
||||
|
||||
As a result, the entire subsystem that generated challenges before had to be ripped out and rewritten from scratch.
|
||||
|
||||
It was replaced with a new flow that stores data on the server side, compares that data against what the client responds with, and then checks pass/fail that way:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: New challenge flow
|
||||
---
|
||||
sequenceDiagram
|
||||
User->>+Anubis: GET /wiki/some-page
|
||||
Anubis->>+Make Challenge: Generate a challenge string
|
||||
Make Challenge->>+Store: Store info for challenge 1234
|
||||
Make Challenge->>-Anubis: Challenge string: taco salad, ID 1234
|
||||
Anubis->>-User: HTTP 401 solve a challenge
|
||||
User->>+Anubis: GET internal-api/pass-challenge, challenge 1234
|
||||
Anubis->>+Validate Challenge: verify challenge 1234
|
||||
Validate Challenge->>+Store: Get info for challenge 1234
|
||||
Store->>-Validate Challenge: Here you go!
|
||||
Validate Challenge->>-Anubis: Valid ✅
|
||||
Anubis->>+User: Here's a cookie to get past Anubis
|
||||
```
|
||||
|
||||
As a result, the [challenge format](#challenge-format-change) had to change. Old cookies will still be validated, but the next minor version (v1.22.0) will include validation to ensure that all challenges are accounted for on the server side. This data is stored in the active [storage backend](/docs/admin/policies/#storage-backends) for up to 30 minutes. This also fixes [#746](https://github.com/TecharoHQ/anubis/issues/746) and other similar instances of this issue.
|
||||
|
||||
### Challenge format change
|
||||
|
||||
Previously Anubis did no accounting for challenges that it issued. This means that if Anubis restarted during a client, the client would be able to proceed once Anubis came back online.
|
||||
|
||||
During the upgrade to v1.21.0 and when v1.21.0 (or later) restarts with the [in-memory storage backend](/docs/admin/policies/#memory), you may see a higher rate of failed challenges than normal. If this persists beyond a few minutes, [open an issue](https://github.com/TecharoHQ/anubis/issues/new).
|
||||
|
||||
If you are using the in-memory storage backend, please consider using [a different storage backend](/docs/admin/policies/#storage-backends).
|
||||
|
||||
### Storage
|
||||
|
||||
Anubis offers a few different storage backends depending on your needs:
|
||||
|
||||
| Backend | Description |
|
||||
| :--------------------------------------- | :------------------------------------------------------------------------------------------------------------- |
|
||||
| [`memory`](/docs/admin/policies/#memory) | An in-memory hashmap that is cleared when Anubis is restarted. |
|
||||
| [`bbolt`](/docs/admin/policies/#bbolt) | A memory-mapped key/value store that can persist between Anubis restarts. |
|
||||
| [`valkey`](/docs/admin/policies/#valkey) | A networked key/value store that can persist between Anubis restarts and coordinate across multiple instances. |
|
||||
|
||||
Please review the documentation for each storage method to figure out the one best for your needs. If you aren't sure, consult this diagram:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: What storage backend do I need?
|
||||
---
|
||||
flowchart TD
|
||||
OneInstance{Do you only have
|
||||
one instance of
|
||||
Anubis?}
|
||||
Persistence{Do you have
|
||||
persistent disk
|
||||
access in your
|
||||
environment?}
|
||||
bbolt[(bbolt)]
|
||||
memory[(memory)]
|
||||
valkey[(valkey)]
|
||||
OneInstance -->|Yes| Persistence
|
||||
OneInstance -->|No| valkey
|
||||
Persistence -->|Yes| bbolt
|
||||
Persistence -->|No| memory
|
||||
```
|
||||
|
||||
## Breaking change: systemd `RuntimeDirectory` change
|
||||
|
||||
The following potentially breaking change applies to native installs with systemd only:
|
||||
|
||||
Each instance of systemd service template now has a unique `RuntimeDirectory`, as opposed to each instance of the service sharing a `RuntimeDirectory`. This change was made to avoid [the `RuntimeDirectory` getting nuked](https://github.com/TecharoHQ/anubis/issues/748) any time one of the Anubis instances restarts.
|
||||
|
||||
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/foo.sock` and additionally configure your HTTP load balancer as appropriate.
|
||||
|
||||
If you need the legacy behaviour, install this [systemd unit dropin](https://www.flatcar.org/docs/latest/setup/systemd/drop-in-units/):
|
||||
|
||||
```systemd
|
||||
# /etc/systemd/system/anubis@.service.d/50-runtimedir.conf
|
||||
[Service]
|
||||
RuntimeDirectory=anubis
|
||||
```
|
||||
|
||||
Just keep in mind that this will cause problems when Anubis restarts.
|
||||
|
||||
## What's up next?
|
||||
|
||||
The biggest things we want to do in the next release (in no particular order):
|
||||
|
||||
- A rewrite of bot checking rule configuration syntax to make it less ambiguous.
|
||||
- [JA4](https://blog.foxio.io/ja4+-network-fingerprinting) (and other forms of) fingerprinting and coordination with [Thoth](/docs/admin/thoth/) to allow clients with high aggregate pass rates through without seeing Anubis at all.
|
||||
- Advanced heuristics for [users of the unbranded variant of Anubis](/docs/admin/botstopper/).
|
||||
- Optimize the release flow so that releases can be triggered and executed by continuous integration tools. The ultimate goal is to make it possible to release Anubis in 15 minutes after pressing a single "mint release" button.
|
||||
- Add "hot reloading" support to Anubis, allowing administrators to update the rules without restarting the service.
|
||||
- Fix [multiple slash support](https://github.com/TecharoHQ/anubis/issues/754) for web applications that require optional path variables.
|
||||
- Add weight to "brand new" clients.
|
||||
- Implement a "maze" feature that tries to get crawlers ensnared in a maze of random links so that clients that are more than 20 links in can be reported to the home base.
|
||||
- Open [Thoth-based advanced checks](/docs/admin/thoth/) to more users with an easier onboarding flow.
|
||||
- More smoke tests including for browsers like [Pale Moon](https://www.palemoon.org/).
|
||||
@@ -13,179 +13,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
<!-- This changes the project to: -->
|
||||
|
||||
- The [Thoth client](https://anubis.techaro.lol/docs/admin/thoth) is now public in the repo instead of being an internal package.
|
||||
|
||||
## v1.21.3: Minfilia Warde - Echo 3
|
||||
|
||||
### Fixes
|
||||
|
||||
#### Fixes a problem with nonstandard URLs and redirects
|
||||
|
||||
Fixes [GHSA-jhjj-2g64-px7c](https://github.com/TecharoHQ/anubis/security/advisories/GHSA-jhjj-2g64-px7c).
|
||||
|
||||
This could allow an attacker to craft an Anubis pass-challenge URL that forces a redirect to nonstandard URLs, such as the `javascript:` scheme which executes arbitrary JavaScript code in a browser context when the user clicks the "Try again" button.
|
||||
|
||||
This has been fixed by disallowing any URLs without the scheme `http` or `https`.
|
||||
|
||||
Additionally, the "Try again" button has been fixed to completely ignore the user-supplied redirect location. It now redirects to the home page (`/`).
|
||||
|
||||
## v1.21.2: Minfilia Warde - Echo 2
|
||||
|
||||
This contained an incomplete fix for [GHSA-jhjj-2g64-px7c](https://github.com/TecharoHQ/anubis/security/advisories/GHSA-jhjj-2g64-px7c). Do not use this version.
|
||||
|
||||
## v1.21.1: Minfilia Warde - Echo 1
|
||||
|
||||
- Expired records are now properly removed from bbolt databases ([#848](https://github.com/TecharoHQ/anubis/pull/848)).
|
||||
- Fix hanging on service restart ([#853](https://github.com/TecharoHQ/anubis/issues/853))
|
||||
|
||||
### Added
|
||||
|
||||
Anubis now supports the [`missingHeader`](./admin/configuration/expressions.mdx#missingHeader) to assert the absence of headers in requests.
|
||||
|
||||
#### New locales
|
||||
|
||||
Anubis now supports these new languages:
|
||||
|
||||
- [Czech](https://github.com/TecharoHQ/anubis/pull/849)
|
||||
- [Finnish](https://github.com/TecharoHQ/anubis/pull/863)
|
||||
- [Norwegian Bokmål](https://github.com/TecharoHQ/anubis/pull/855)
|
||||
- [Norwegian Nynorsk](https://github.com/TecharoHQ/anubis/pull/855)
|
||||
- [Russian](https://github.com/TecharoHQ/anubis/pull/882)
|
||||
|
||||
### Fixes
|
||||
|
||||
#### Fix ["error: can't get challenge"](https://github.com/TecharoHQ/anubis/issues/869) when details about a challenge can't be found in the server side state
|
||||
|
||||
v1.21.0 changed the core challenge flow to maintain information about challenges on the server side instead of only doing them via stateless idempotent generation functions and relying on details to not change. There was a subtle bug introduced in this change: if a client has an unknown challenge ID set in its test cookie, Anubis will clear that cookie and then throw an HTTP 500 error.
|
||||
|
||||
This has been fixed by making Anubis throw a new challenge page instead.
|
||||
|
||||
#### Fix event loop thrashing when solving a proof of work challenge
|
||||
|
||||
Previously the "fast" proof of work solver had a fragment of JavaScript that attempted to only post an update about proof of work progress to the main browser window every 1024 iterations. This fragment of JavaScript was subtly incorrect in a way that passed review but actually made the workers send an update back to the main thread every iteration. This caused a pileup of unhandled async calls (similar to a socket accept() backlog pileup in Unix) that caused stack space exhaustion.
|
||||
|
||||
This has been fixed in the following ways:
|
||||
|
||||
1. The complicated boolean logic has been totally removed in favour of a worker-local iteration counter.
|
||||
2. The progress bar is updated by worker `0` instead of all workers.
|
||||
|
||||
Hopefully this should limit the event loop thrashing and let ia32 browsers (as well as any environment with a smaller stack size than amd64 and aarch64 seem to have) function normally when processing Anubis proof of work challenges.
|
||||
|
||||
#### Fix potential memory leak when discovering a solution
|
||||
|
||||
In some cases, the parallel solution finder in Anubis could cause all of the worker promises to leak due to the fact the promises were being improperly terminated. This was fixed by having Anubis debounce worker termination instead of allowing it to potentially recurse infinitely.
|
||||
|
||||
## v1.21.0: Minfilia Warde
|
||||
|
||||
> Please, be at ease. You are among friends here.
|
||||
|
||||
In this release, Anubis becomes internationalized, gains the ability to use system load as input to issuing challenges, finally fixes the "invalid response" after "success" bug, and more! Please read these notes before upgrading as the changes are big enough that administrators should take action to ensure that the upgrade goes smoothly.
|
||||
|
||||
### Big ticket changes
|
||||
|
||||
The biggest change is that the ["invalid response" after "success" bug](https://github.com/TecharoHQ/anubis/issues/564) is now finally fixed for good by totally rewriting how Anubis' challenge issuance flow works. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. This also fixes [#746](https://github.com/TecharoHQ/anubis/issues/746) and other similar instances of this issue.
|
||||
|
||||
In order to reduce confusion, the "Success" interstitial that shows up when you pass a proof of work challenge has been removed.
|
||||
|
||||
#### Storage
|
||||
|
||||
Anubis now is able to store things persistently [in memory](./admin/policies.mdx#memory), [on the disk](./admin/policies.mdx#bbolt), or [in Valkey](./admin/policies.mdx#valkey) (this includes other compatible software). By default Anubis uses the in-memory backend. If you have an environment with mutable storage (even if it is temporary), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend.
|
||||
|
||||
#### Localization
|
||||
|
||||
Anubis now supports localized responses. Locales can be added in [lib/localization/locales/](https://github.com/TecharoHQ/anubis/tree/main/lib/localization/locales). This release includes support for the following languages:
|
||||
|
||||
- [Brazilian Portugese](https://github.com/TecharoHQ/anubis/pull/726)
|
||||
- [Chinese (Simplified)](https://github.com/TecharoHQ/anubis/pull/774)
|
||||
- [Chinese (Traditional)](https://github.com/TecharoHQ/anubis/pull/759)
|
||||
- English
|
||||
- [Estonian](https://github.com/TecharoHQ/anubis/pull/783)
|
||||
- [Filipino](https://github.com/TecharoHQ/anubis/pull/775)
|
||||
- [French](https://github.com/TecharoHQ/anubis/pull/716)
|
||||
- [German](https://github.com/TecharoHQ/anubis/pull/741)
|
||||
- [Icelandic](https://github.com/TecharoHQ/anubis/pull/780)
|
||||
- [Italian](https://github.com/TecharoHQ/anubis/pull/778)
|
||||
- [Japanese](https://github.com/TecharoHQ/anubis/pull/772)
|
||||
- [Spanish](https://github.com/TecharoHQ/anubis/pull/716)
|
||||
- [Turkish](https://github.com/TecharoHQ/anubis/pull/751)
|
||||
|
||||
If facts or local regulations demand, you can set Anubis default language with the `FORCED_LANGUAGE` environment variable or the `--forced-language` command line argument:
|
||||
|
||||
```sh
|
||||
FORCED_LANGUAGE=de
|
||||
```
|
||||
|
||||
#### Load average
|
||||
|
||||
Anubis can dynamically take action [based on the system load average](./admin/configuration/expressions.mdx#using-the-system-load-average), allowing you to write rules like this:
|
||||
|
||||
```yaml
|
||||
## System load based checks.
|
||||
# If the system is under high load for the last minute, add weight.
|
||||
- name: high-load-average
|
||||
action: WEIGH
|
||||
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: 20
|
||||
|
||||
# If it is not for the last 15 minutes, remove weight.
|
||||
- name: low-load-average
|
||||
action: WEIGH
|
||||
expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: -10
|
||||
```
|
||||
|
||||
Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example:
|
||||
|
||||
| Kind | Core count | Load threshold |
|
||||
| --------: | :--------- | :------------- |
|
||||
| high load | 4 | `8.0` |
|
||||
| low load | 4 | `2.0` |
|
||||
| high load | 16 | `32.0` |
|
||||
| low load | 16 | `8` |
|
||||
|
||||
Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
|
||||
|
||||
### Other features and fixes
|
||||
|
||||
There are a bunch of other assorted features and fixes too:
|
||||
|
||||
- Add `COOKIE_SECURE` option to set the cookie [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies)
|
||||
- Sets cookie defaults to use [SameSite: None](https://web.dev/articles/samesite-cookies-explained)
|
||||
- Determine the `BIND_NETWORK`/`--bind-network` value from the bind address ([#677](https://github.com/TecharoHQ/anubis/issues/677)).
|
||||
- Implement localization system. Find locale files in lib/localization/locales/.
|
||||
- Implement a [development container](https://containers.dev/) manifest to make contributions easier.
|
||||
- Fix dynamic cookie domains functionality ([#731](https://github.com/TecharoHQ/anubis/pull/731))
|
||||
- Add option for custom cookie prefix ([#732](https://github.com/TecharoHQ/anubis/pull/732))
|
||||
- Add translation for German language ([#741](https://github.com/TecharoHQ/anubis/pull/741))
|
||||
- Remove the "Success" interstitial after a proof of work challenge is concluded.
|
||||
- Anubis now has the concept of [storage backends](./admin/policies.mdx#storage-backends). These allow you to change how Anubis stores temporary data (in memory, on the disk, or in Valkey). If you run Anubis in an environment where you have a low amount of memory available for Anubis (eg: less than 64 megabytes), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend.
|
||||
- The challenge issuance and validation process has been rewritten from scratch. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. Fixes [#564](https://github.com/TecharoHQ/anubis/issues/564), [#746](https://github.com/TecharoHQ/anubis/issues/746), and other similar instances of this issue.
|
||||
- Make the [Open Graph](./admin/configuration/open-graph.mdx) subsystem and DNSBL subsystem use [storage backends](./admin/policies.mdx#storage-backends) instead of storing everything in memory by default.
|
||||
- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
|
||||
- Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751))
|
||||
- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
|
||||
- The [bbolt storage backend](./admin/policies.mdx#bbolt) now runs its cleanup every hour instead of every five minutes.
|
||||
- Don't block Anubis starting up if [Thoth](./admin/thoth.mdx) health checks fail.
|
||||
- A race condition involving [opening two challenge pages at once in different tabs](https://github.com/TecharoHQ/anubis/issues/832) causing one of them to fail has been fixed.
|
||||
- The "Try again" button on the error page has been fixed. Previously it meant "try the solution again" instead of "try the challenge again".
|
||||
- In certain cases, a user could be stuck with a test cookie that is invalid, locking them out of the service for up to half an hour. This has been fixed with better validation of this case and clearing the cookie.
|
||||
- Start exposing JA4H fingerprints for later use in CEL expressions.
|
||||
- Add `/healthz` route for use in platform-based health checks.
|
||||
- Added the ability for Anubis to dynamically take action [based on the system load average](./admin/configuration/expressions.mdx#using-the-system-load-average).
|
||||
- Add translation for Traditional Chinese ([#759](https://github.com/TecharoHQ/anubis/pull/759))
|
||||
|
||||
### Potentially breaking changes
|
||||
|
||||
We try to introduce breaking changes as much as possible, but these are the changes that may be relevant for you as an administrator:
|
||||
|
||||
#### Challenge format change
|
||||
|
||||
Previously Anubis did no accounting for challenges that it issued. This means that if Anubis restarted during a client, the client would be able to proceed once Anubis came back online.
|
||||
|
||||
During the upgrade to v1.21.0 and when v1.21.0 (or later) restarts with the [in-memory storage backend](./admin/policies.mdx#memory), you may see a higher rate of failed challenges than normal. If this persists beyond a few minutes, [open an issue](https://github.com/TecharoHQ/anubis/issues/new).
|
||||
|
||||
If you are using the in-memory storage backend, please consider using [a different storage backend](./admin/policies.mdx#storage-backends).
|
||||
|
||||
#### Systemd service changes
|
||||
|
||||
The following potentially breaking change applies to native installs with systemd only:
|
||||
|
||||
Each instance of systemd service template now has a unique `RuntimeDirectory`, as opposed to each instance of the service sharing a `RuntimeDirectory`. This change was made to avoid [the `RuntimeDirectory` getting nuked any time one of the Anubis instances restarts](https://github.com/TecharoHQ/anubis/issues/748).
|
||||
|
||||
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/foo.sock` and additionally configure your HTTP load balancer as appropriate.
|
||||
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/sock` and additionally configure your HTTP load balancer as appropriate.
|
||||
|
||||
If you need the legacy behaviour, install this [systemd unit dropin](https://www.flatcar.org/docs/latest/setup/systemd/drop-in-units/):
|
||||
|
||||
@@ -195,8 +48,6 @@ If you need the legacy behaviour, install this [systemd unit dropin](https://www
|
||||
RuntimeDirectory=anubis
|
||||
```
|
||||
|
||||
Just keep in mind that this will cause problems when Anubis restarts.
|
||||
|
||||
## v1.20.0: Thancred Waters
|
||||
|
||||
The big ticket items are as follows:
|
||||
|
||||
@@ -77,7 +77,7 @@ For example, consider this rule:
|
||||
|
||||
For this rule, if a request comes in from `8.8.8.8` or `1.1.1.1`, Anubis will deny the request and return an error page.
|
||||
|
||||
### `all` blocks
|
||||
#### `all` blocks
|
||||
|
||||
An `all` block that contains a list of expressions. If all expressions in the list return `true`, then the action specified in the rule will be taken. If any of the expressions in the list returns `false`, Anubis will move on to the next rule.
|
||||
|
||||
@@ -123,7 +123,7 @@ In order to avoid this, make sure the header or query parameter you are testing
|
||||
- 'path == "/index.php"'
|
||||
- '"title" in query'
|
||||
- '"action" in query'
|
||||
- 'query["action"] == "history"'
|
||||
- 'query["action"] == "history"
|
||||
```
|
||||
|
||||
This rule throws a challenge if and only if all of the following conditions are true:
|
||||
@@ -186,32 +186,8 @@ Also keep in mind that this does not account for other kinds of latency like I/O
|
||||
|
||||
Anubis expressions can be augmented with the following functions:
|
||||
|
||||
### `missingHeader`
|
||||
|
||||
Available in `bot` expressions.
|
||||
|
||||
```ts
|
||||
function missingHeader(headers: Record<string, string>, key: string) bool
|
||||
```
|
||||
|
||||
`missingHeader` returns `true` if the request does not contain a header. This is useful when you are trying to assert behavior such as:
|
||||
|
||||
```yaml
|
||||
# Adds weight to old versions of Chrome
|
||||
- name: old-chrome
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: 10
|
||||
expression:
|
||||
all:
|
||||
- userAgent.matches("Chrome/[1-9][0-9]?\\.0\\.0\\.0")
|
||||
- missingHeader(headers, "Sec-Ch-Ua")
|
||||
```
|
||||
|
||||
### `randInt`
|
||||
|
||||
Available in all expressions.
|
||||
|
||||
```ts
|
||||
function randInt(n: int): int;
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ Effectively you have one trip through Apache to do TLS termination, a detour thr
|
||||
|
||||
:::note
|
||||
|
||||
These examples assume that you are using a setup where your Apache configuration is made up of a bunch of files in `/etc/httpd/conf.d/*.conf`. This is not true for all deployments of Apache. If you are not in such an environment, append these snippets to your `/etc/httpd/conf/httpd.conf` file.
|
||||
These examples assume that you are using a setup where your nginx configuration is made up of a bunch of files in `/etc/httpd/conf.d/*.conf`. This is not true for all deployments of Apache. If you are not in such an environment, append these snippets to your `/etc/httpd/conf/httpd.conf` file.
|
||||
|
||||
:::
|
||||
|
||||
@@ -56,7 +56,6 @@ Assuming you are protecting `anubistest.techaro.lol`, you need the following ser
|
||||
</VirtualHost>
|
||||
|
||||
# HTTPS listener that forwards to Anubis
|
||||
<IfModule mod_proxy.c>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin your@email.here
|
||||
ServerName anubistest.techaro.lol
|
||||
|
||||
@@ -4,7 +4,7 @@ Docker compose is typically used in concert with other load balancers such as [A
|
||||
|
||||
```yaml
|
||||
services:
|
||||
anubis:
|
||||
anubis-nginx:
|
||||
image: ghcr.io/techarohq/anubis:latest
|
||||
environment:
|
||||
BIND: ":8080"
|
||||
@@ -15,17 +15,10 @@ services:
|
||||
POLICY_FNAME: "/data/cfg/botPolicy.yaml"
|
||||
OG_PASSTHROUGH: "true"
|
||||
OG_EXPIRY_TIME: "24h"
|
||||
healthcheck:
|
||||
test: ["CMD", "anubis", "--healthcheck"]
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 500ms
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- "./botPolicy.yaml:/data/cfg/botPolicy.yaml:ro"
|
||||
|
||||
nginx:
|
||||
image: nginx
|
||||
volumes:
|
||||
|
||||
@@ -79,10 +79,6 @@ server {
|
||||
root "/srv/http/anubistest.techaro.lol";
|
||||
index index.html;
|
||||
|
||||
# Get the visiting IP from the TLS termination server
|
||||
set_real_ip_from unix:;
|
||||
real_ip_header X-Real-IP;
|
||||
|
||||
# Your normal configuration can go here
|
||||
# location .php { fastcgi...} etc.
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ Anubis uses these environment variables for configuration:
|
||||
| `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_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
||||
| `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. When running multiple instances on the same base domain, the key must be the same across all instances. 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. |
|
||||
| `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. |
|
||||
|
||||
@@ -137,7 +137,7 @@ Test to make sure it's running with `curl`:
|
||||
curl http://localhost:8240/metrics
|
||||
```
|
||||
|
||||
Then set up your reverse proxy (Nginx, Caddy, etc.) to point to the Anubis port. Anubis will then reverse proxy all requests that meet the policies in `/etc/anubis/gitea.botPolicies.yaml` to the target service.
|
||||
Then set up your reverse proxy (Nginx, Caddy, etc.) to point to the Anubis port. Anubis will then reverse proxy all requests that meet the policies in `/etc/anubis/gitea.botPolicies.json` to the target service.
|
||||
|
||||
For more details on particular reverse proxies, see here:
|
||||
|
||||
|
||||
@@ -268,12 +268,6 @@ The memory backend is an in-memory cache. This backend works best if you don't u
|
||||
|
||||
The biggest downside is that there is not currently a limit to how much data can be stored in memory. This will be addressed at a later time.
|
||||
|
||||
:::warning
|
||||
|
||||
The in-memory backend exists mostly for validation, testing, and to ensure that the default configuration of Anubis works as expected. Do not use this persistently in production.
|
||||
|
||||
:::
|
||||
|
||||
#### Configuration
|
||||
|
||||
The memory backend does not require any configuration to use.
|
||||
|
||||
@@ -65,13 +65,6 @@ Anubis is brought to you by sponsors and donors like:
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://fabulous.systems/">
|
||||
<img
|
||||
src="/img/sponsors/fabulous-systems.webp"
|
||||
alt="Cat eyes over the word Emma in a serif font"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -51,13 +51,6 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
||||
- https://pluralpedia.org/
|
||||
- https://git.aya.so/
|
||||
- https://marginalia-search.com/
|
||||
- https://repositorio.ufrn.br/home/
|
||||
- https://mozillazine.org/
|
||||
- https://clew.se/
|
||||
- https://tumfatig.net/
|
||||
- https://rpmfusion.org/
|
||||
- https://wiki.freepascal.org/
|
||||
- https://azurlane.koumakan.jp/
|
||||
- <details>
|
||||
<summary>FreeCAD</summary>
|
||||
- https://forum.freecad.org/
|
||||
@@ -106,10 +99,3 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
||||
<summary>Forschungszentrum Jülich</summary>
|
||||
- https://juser.fz-juelich.de/
|
||||
</details>
|
||||
- <details>
|
||||
<summary>archlinux32.org</summary>
|
||||
- https://www.archlinux32.org/packages/
|
||||
- https://bbs.archlinux32.org/
|
||||
- https://bugs.archlinux32.org/
|
||||
</details>
|
||||
|
||||
|
||||
@@ -139,10 +139,6 @@ const config: Config = {
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/TecharoHQ/anubis',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
href: 'https://techarohq.github.io/status/'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
app = 'anubis-docs'
|
||||
primary_region = 'yyz'
|
||||
|
||||
[build]
|
||||
image = "ghcr.io/techarohq/anubis/docs:main"
|
||||
|
||||
[http_service]
|
||||
internal_port = 80
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ['app']
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
memory_mb = 256
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/javascript js;
|
||||
application/atom+xml atom;
|
||||
application/rss+xml rss;
|
||||
|
||||
text/mathml mml;
|
||||
text/plain txt;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
text/vnd.wap.wml wml;
|
||||
text/x-component htc;
|
||||
|
||||
image/avif avif;
|
||||
image/png png;
|
||||
image/svg+xml svg svgz;
|
||||
image/tiff tif tiff;
|
||||
image/vnd.wap.wbmp wbmp;
|
||||
image/webp webp;
|
||||
image/x-icon ico;
|
||||
image/x-jng jng;
|
||||
image/x-ms-bmp bmp;
|
||||
|
||||
font/woff woff;
|
||||
font/woff2 woff2;
|
||||
|
||||
application/java-archive jar war ear;
|
||||
application/json json;
|
||||
application/mac-binhex40 hqx;
|
||||
application/msword doc;
|
||||
application/pdf pdf;
|
||||
application/postscript ps eps ai;
|
||||
application/rtf rtf;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
application/vnd.google-earth.kml+xml kml;
|
||||
application/vnd.google-earth.kmz kmz;
|
||||
application/vnd.ms-excel xls;
|
||||
application/vnd.ms-fontobject eot;
|
||||
application/vnd.ms-powerpoint ppt;
|
||||
application/vnd.oasis.opendocument.graphics odg;
|
||||
application/vnd.oasis.opendocument.presentation odp;
|
||||
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||
application/vnd.oasis.opendocument.text odt;
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
pptx;
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
xlsx;
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
docx;
|
||||
application/vnd.wap.wmlc wmlc;
|
||||
application/wasm wasm;
|
||||
application/x-7z-compressed 7z;
|
||||
application/x-cocoa cco;
|
||||
application/x-java-archive-diff jardiff;
|
||||
application/x-java-jnlp-file jnlp;
|
||||
application/x-makeself run;
|
||||
application/x-perl pl pm;
|
||||
application/x-pilot prc pdb;
|
||||
application/x-rar-compressed rar;
|
||||
application/x-redhat-package-manager rpm;
|
||||
application/x-sea sea;
|
||||
application/x-shockwave-flash swf;
|
||||
application/x-stuffit sit;
|
||||
application/x-tcl tcl tk;
|
||||
application/x-x509-ca-cert der pem crt;
|
||||
application/x-xpinstall xpi;
|
||||
application/xhtml+xml xhtml;
|
||||
application/xspf+xml xspf;
|
||||
application/zip zip;
|
||||
|
||||
application/octet-stream bin exe dll;
|
||||
application/octet-stream deb;
|
||||
application/octet-stream dmg;
|
||||
application/octet-stream iso img;
|
||||
application/octet-stream msi msp msm;
|
||||
|
||||
audio/midi mid midi kar;
|
||||
audio/mpeg mp3;
|
||||
audio/ogg ogg;
|
||||
audio/x-m4a m4a;
|
||||
audio/x-realaudio ra;
|
||||
|
||||
video/3gpp 3gpp 3gp;
|
||||
video/mp2t ts;
|
||||
video/mp4 mp4;
|
||||
video/mpeg mpeg mpg;
|
||||
video/quicktime mov;
|
||||
video/webm webm;
|
||||
video/x-flv flv;
|
||||
video/x-m4v m4v;
|
||||
video/x-mng mng;
|
||||
video/x-ms-asf asx asf;
|
||||
video/x-ms-wmv wmv;
|
||||
video/x-msvideo avi;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
user nginx;
|
||||
worker_processes 2;
|
||||
error_log /dev/stdout warn;
|
||||
pid /nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /dev/stdout;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
root /www;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,6 @@ spec:
|
||||
- name: anubis
|
||||
configMap:
|
||||
name: anubis-cfg
|
||||
- name: nginx
|
||||
configMap:
|
||||
name: nginx-cfg
|
||||
- name: temporary-data
|
||||
emptyDir: {}
|
||||
containers:
|
||||
@@ -31,23 +28,8 @@ spec:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
volumeMounts:
|
||||
- name: nginx
|
||||
mountPath: /conf
|
||||
ports:
|
||||
- containerPort: 80
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
- name: anubis
|
||||
image: ghcr.io/techarohq/anubis:main
|
||||
imagePullPolicy: Always
|
||||
@@ -93,15 +75,3 @@ spec:
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: anubis-docs-thoth
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 9090
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 9090
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
|
||||
@@ -11,8 +11,3 @@ configMapGenerator:
|
||||
behavior: create
|
||||
files:
|
||||
- ./cfg/anubis/botPolicies.yaml
|
||||
- name: nginx-cfg
|
||||
behavior: create
|
||||
files:
|
||||
- ./cfg/nginx/mime.types
|
||||
- ./cfg/nginx/nginx.conf
|
||||
|
||||
20
docs/package-lock.json
generated
20
docs/package-lock.json
generated
@@ -5908,9 +5908,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -6496,16 +6496,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
|
||||
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"on-headers": "~1.0.2",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
@@ -13562,9 +13562,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
|
||||
BIN
docs/static/img/sponsors/fabulous-systems.webp
vendored
BIN
docs/static/img/sponsors/fabulous-systems.webp
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,7 +0,0 @@
|
||||
package anubis
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrMisconfiguration = errors.New("[unexpected] policy: administrator misconfiguration")
|
||||
)
|
||||
19
go.mod
19
go.mod
@@ -7,24 +7,23 @@ require (
|
||||
github.com/a-h/templ v0.3.906
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||
github.com/gaissmai/bart v0.20.5
|
||||
github.com/gaissmai/bart v0.20.4
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/cel-go v0.25.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
github.com/playwright-community/playwright-go v0.5200.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||
github.com/shirou/gopsutil/v4 v4.25.6
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/testcontainers/testcontainers-go v0.37.0
|
||||
go.etcd.io/bbolt v1.4.2
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/text v0.27.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/text v0.26.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.33.2
|
||||
@@ -70,7 +69,7 @@ require (
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
@@ -158,15 +157,15 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.3 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // 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/mod v0.25.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
|
||||
golang.org/x/term v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/vuln v1.1.4 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
|
||||
38
go.sum
38
go.sum
@@ -109,8 +109,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
|
||||
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
@@ -133,8 +133,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gaissmai/bart v0.20.5 h1:ehoWZWQ7j//qt0K0Zs4i9hpoPpbgqsMQiR8W2QPJh+c=
|
||||
github.com/gaissmai/bart v0.20.5/go.mod h1:cEed+ge8dalcbpi8wtS9x9m2hn/fNJH5suhdGQOHnYk=
|
||||
github.com/gaissmai/bart v0.20.4 h1:Ik47r1fy3jRVU+1eYzKSW3ho2UgBVTVnUS8O993584U=
|
||||
github.com/gaissmai/bart v0.20.4/go.mod h1:cEed+ge8dalcbpi8wtS9x9m2hn/fNJH5suhdGQOHnYk=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
@@ -253,8 +253,6 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be h1:dVIND0nXGXPQnFZYrMXT6CxHhBYhTPMm0GFqcmfaIC4=
|
||||
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be/go.mod h1:q68TUR45WDa2r3yU4aO6WgxfCc0Vj1qtRaKaRE3yMLM=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
@@ -335,8 +333,8 @@ github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
@@ -429,8 +427,8 @@ 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-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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
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/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
|
||||
@@ -450,8 +448,8 @@ 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.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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -459,8 +457,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -487,8 +485,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -496,8 +494,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -506,8 +504,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/health"
|
||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
var HealthSrv = health.NewServer()
|
||||
|
||||
func SetHealth(svc string, status healthv1.HealthCheckResponse_ServingStatus) {
|
||||
HealthSrv.SetServingStatus(svc, status)
|
||||
}
|
||||
|
||||
func GetHealth(svc string) (healthv1.HealthCheckResponse_ServingStatus, bool) {
|
||||
st, err := HealthSrv.Check(context.Background(), &healthv1.HealthCheckRequest{
|
||||
Service: svc,
|
||||
})
|
||||
if err != nil {
|
||||
return healthv1.HealthCheckResponse_UNKNOWN, false
|
||||
}
|
||||
|
||||
return st.GetStatus(), true
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/lum8rjack/go-ja4h"
|
||||
)
|
||||
|
||||
func JA4H(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Add("X-Http-Fingerprint-JA4H", ja4h.JA4H(r))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
iptoasnv1 "github.com/TecharoHQ/thoth-proto/gen/techaro/thoth/iptoasn/v1"
|
||||
)
|
||||
|
||||
func (c *Client) ASNCheckerFor(asns []uint32) checker.Interface {
|
||||
func (c *Client) ASNCheckerFor(asns []uint32) checker.Impl {
|
||||
asnMap := map[uint32]struct{}{}
|
||||
var sb strings.Builder
|
||||
fmt.Fprintln(&sb, "ASNChecker")
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||
"github.com/TecharoHQ/anubis/internal/thoth"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
iptoasnv1 "github.com/TecharoHQ/thoth-proto/gen/techaro/thoth/iptoasn/v1"
|
||||
)
|
||||
|
||||
var _ checker.Interface = &thoth.ASNChecker{}
|
||||
var _ checker.Impl = &thoth.ASNChecker{}
|
||||
|
||||
func TestASNChecker(t *testing.T) {
|
||||
cli := loadSecrets(t)
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
iptoasnv1 "github.com/TecharoHQ/thoth-proto/gen/techaro/thoth/iptoasn/v1"
|
||||
)
|
||||
|
||||
func (c *Client) GeoIPCheckerFor(countries []string) checker.Interface {
|
||||
func (c *Client) GeoIPCheckerFor(countries []string) checker.Impl {
|
||||
countryMap := map[string]struct{}{}
|
||||
var sb strings.Builder
|
||||
fmt.Fprintln(&sb, "GeoIPChecker")
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||
"github.com/TecharoHQ/anubis/internal/thoth"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
)
|
||||
|
||||
var _ checker.Interface = &thoth.GeoIPChecker{}
|
||||
var _ checker.Impl = &thoth.GeoIPChecker{}
|
||||
|
||||
func TestGeoIPChecker(t *testing.T) {
|
||||
cli := loadSecrets(t)
|
||||
@@ -60,6 +60,15 @@ func New(ctx context.Context, thothURL, apiToken string, plaintext bool) (*Clien
|
||||
|
||||
hc := healthv1.NewHealthClient(conn)
|
||||
|
||||
resp, err := hc.Check(ctx, &healthv1.HealthCheckRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't verify thoth health at %s: %w", thothURL, err)
|
||||
}
|
||||
|
||||
if resp.Status != healthv1.HealthCheckResponse_SERVING {
|
||||
return nil, fmt.Errorf("thoth is not healthy, wanted %s but got %s", healthv1.HealthCheckResponse_SERVING, resp.Status)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
conn: conn,
|
||||
health: hc,
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||
"github.com/TecharoHQ/anubis/lib/thoth/thothmock"
|
||||
"github.com/TecharoHQ/anubis/internal/thoth"
|
||||
"github.com/TecharoHQ/anubis/internal/thoth/thothmock"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||
"github.com/TecharoHQ/anubis/internal/thoth"
|
||||
)
|
||||
|
||||
func WithMockThoth(t *testing.T) context.Context {
|
||||
@@ -28,17 +28,15 @@ import (
|
||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
|
||||
// checker implementations
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/all"
|
||||
|
||||
// challenge implementations
|
||||
_ "github.com/TecharoHQ/anubis/lib/challenge/all"
|
||||
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
||||
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -104,10 +102,6 @@ func (s *Server) challengeFor(r *http.Request) (*challenge.Challenge, error) {
|
||||
ckie := ckies[0]
|
||||
chall, err := j.Get(r.Context(), "challenge:"+ckie.Value)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return s.issueChallenge(r.Context(), r)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -386,23 +380,6 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
lg := internal.GetRequestLogger(r)
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
redir := r.FormValue("redir")
|
||||
redirURL, err := url.ParseRequestURI(redir)
|
||||
if err != nil {
|
||||
lg.Error("invalid redirect", "err", err)
|
||||
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch redirURL.Scheme {
|
||||
case "", "http", "https":
|
||||
// allowed
|
||||
default:
|
||||
lg.Error("XSS attempt blocked, invalid redirect scheme", "scheme", redirURL.Scheme)
|
||||
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Adjust cookie path if base prefix is not empty
|
||||
cookiePath := "/"
|
||||
if anubis.BasePrefix != "" {
|
||||
@@ -417,6 +394,15 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
|
||||
|
||||
redir := r.FormValue("redir")
|
||||
redirURL, err := url.ParseRequestURI(redir)
|
||||
if err != nil {
|
||||
lg.Error("invalid redirect", "err", err)
|
||||
s.respondWithError(w, r, localizer.T("invalid_redirect"))
|
||||
return
|
||||
}
|
||||
// used by the path checker rule
|
||||
r.URL = redirURL
|
||||
|
||||
@@ -551,7 +537,7 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
|
||||
if matches {
|
||||
return cr("threshold/"+t.Name, t.Action, weight), &policy.Bot{
|
||||
Challenge: t.Challenge,
|
||||
Rules: &checker.Any{},
|
||||
Rules: &checker.List{},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -562,6 +548,6 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
|
||||
ReportAs: s.policy.DefaultDifficulty,
|
||||
Algorithm: config.DefaultAlgorithm,
|
||||
},
|
||||
Rules: &checker.Any{},
|
||||
Rules: &checker.List{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -17,9 +15,9 @@ import (
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/thoth/thothmock"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/thoth/thothmock"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -738,230 +736,3 @@ func TestStripBasePrefixFromRequest(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestChallengeFor_ErrNotFound makes sure that users with invalid challenge IDs
|
||||
// in the test cookie don't get rejected by the database lookup failing.
|
||||
func TestChallengeFor_ErrNotFound(t *testing.T) {
|
||||
pol := loadPolicies(t, "testdata/aggressive_403.yaml", 0)
|
||||
ckieExpiration := 10 * time.Minute
|
||||
const wrongCookie = "wrong cookie"
|
||||
|
||||
srv := spawnAnubis(t, Options{
|
||||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: "127.0.0.1",
|
||||
CookieExpiration: ckieExpiration,
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
req.Header.Set("X-Real-IP", "127.0.0.1")
|
||||
req.Header.Set("User-Agent", "CHALLENGE")
|
||||
req.AddCookie(&http.Cookie{Name: anubis.TestCookieName, Value: wrongCookie})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.maybeReverseProxyOrPage(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
body := new(strings.Builder)
|
||||
_, err := io.Copy(body, resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("reading body should not fail: %v", err)
|
||||
}
|
||||
|
||||
t.Run("make sure challenge page is issued", func(t *testing.T) {
|
||||
if !strings.Contains(body.String(), "anubis_challenge") {
|
||||
t.Error("should get a challenge page")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("should get a 401 Unauthorized, got: %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("make sure that the body is not an error page", func(t *testing.T) {
|
||||
if strings.Contains(body.String(), "reject.webp") {
|
||||
t.Error("should not get an internal server error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("make sure new test cookie is issued", func(t *testing.T) {
|
||||
found := false
|
||||
for _, cookie := range resp.Cookies() {
|
||||
if cookie.Name == anubis.TestCookieName {
|
||||
if cookie.Value == wrongCookie {
|
||||
t.Error("a new challenge cookie should be issued")
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("a new test cookie should be set")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPassChallengeXSS(t *testing.T) {
|
||||
pol := loadPolicies(t, "", anubis.DefaultDifficulty)
|
||||
|
||||
srv := spawnAnubis(t, Options{
|
||||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
defer ts.Close()
|
||||
|
||||
cli := httpClient(t)
|
||||
chall := makeChallenge(t, ts, cli)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
redir string
|
||||
}{
|
||||
{
|
||||
name: "javascript alert",
|
||||
redir: "javascript:alert('xss')",
|
||||
},
|
||||
{
|
||||
name: "vbscript",
|
||||
redir: "vbscript:msgbox(\"XSS\")",
|
||||
},
|
||||
{
|
||||
name: "data url",
|
||||
redir: "data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=",
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("with test cookie", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nonce := 0
|
||||
elapsedTime := 420
|
||||
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", tc.redir)
|
||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
u, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, ckie := range cli.Jar.Cookies(u) {
|
||||
if ckie.Name == anubis.TestCookieName {
|
||||
req.AddCookie(ckie)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("can't do request: %v", err)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if bytes.Contains(body, []byte(tc.redir)) {
|
||||
t.Log(string(body))
|
||||
t.Error("found XSS in HTML body")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("wanted status %d, got %d. body: %s", http.StatusBadRequest, resp.StatusCode, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no test cookie", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nonce := 0
|
||||
elapsedTime := 420
|
||||
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", tc.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)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if bytes.Contains(body, []byte(tc.redir)) {
|
||||
t.Log(string(body))
|
||||
t.Error("found XSS in HTML body")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("wanted status %d, got %d. body: %s", http.StatusBadRequest, resp.StatusCode, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestXForwardedForNoDoubleComma(t *testing.T) {
|
||||
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
||||
fmt.Fprintln(w, "OK")
|
||||
})
|
||||
|
||||
h = internal.XForwardedForToXRealIP(h)
|
||||
h = internal.XForwardedForUpdate(false, h)
|
||||
|
||||
pol := loadPolicies(t, "testdata/permissive.yaml", 4)
|
||||
|
||||
srv := spawnAnubis(t, Options{
|
||||
Next: h,
|
||||
Policy: pol,
|
||||
})
|
||||
ts := httptest.NewServer(srv)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Real-Ip", "10.0.0.1")
|
||||
|
||||
resp, err := ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("response status is wrong, wanted %d but got: %s", http.StatusOK, resp.Status)
|
||||
}
|
||||
|
||||
if xff := resp.Header.Get("X-Forwarded-For"); strings.HasPrefix(xff, ",,") {
|
||||
t.Errorf("X-Forwarded-For has two leading commas: %q", xff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package all
|
||||
|
||||
import (
|
||||
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
||||
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
||||
)
|
||||
@@ -1,35 +0,0 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
)
|
||||
|
||||
type All []Interface
|
||||
|
||||
func (a All) Check(r *http.Request) (bool, error) {
|
||||
for _, c := range a {
|
||||
match, err := c.Check(r)
|
||||
if err != nil {
|
||||
return match, err
|
||||
}
|
||||
if !match {
|
||||
return false, err // no match
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil // match
|
||||
}
|
||||
|
||||
func (a All) Hash() string {
|
||||
var sb strings.Builder
|
||||
|
||||
for _, c := range a {
|
||||
fmt.Fprintln(&sb, c.Hash())
|
||||
}
|
||||
|
||||
return internal.FastHash(sb.String())
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Package all imports all of the standard checker types.
|
||||
package all
|
||||
|
||||
import (
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/expression"
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/headerexists"
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/headermatches"
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/path"
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/remoteaddress"
|
||||
)
|
||||
@@ -1,70 +0,0 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAll_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
checkers []MockChecker
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "All match",
|
||||
checkers: []MockChecker{
|
||||
{Result: true, Err: nil},
|
||||
{Result: true, Err: nil},
|
||||
},
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "One not match",
|
||||
checkers: []MockChecker{
|
||||
{Result: true, Err: nil},
|
||||
{Result: false, Err: nil},
|
||||
},
|
||||
want: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "No match",
|
||||
checkers: []MockChecker{
|
||||
{Result: false, Err: nil},
|
||||
{Result: false, Err: nil},
|
||||
},
|
||||
want: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Error encountered",
|
||||
checkers: []MockChecker{
|
||||
{Result: true, Err: nil},
|
||||
{Result: false, Err: http.ErrNotSupported},
|
||||
},
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var all All
|
||||
for _, mc := range tt.checkers {
|
||||
all = append(all, mc)
|
||||
}
|
||||
|
||||
got, err := all.Check(nil)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("All.Check() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("All.Check() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
)
|
||||
|
||||
type Any []Interface
|
||||
|
||||
func (a Any) Check(r *http.Request) (bool, error) {
|
||||
for _, c := range a {
|
||||
match, err := c.Check(r)
|
||||
if err != nil {
|
||||
return match, err
|
||||
}
|
||||
if match {
|
||||
return true, err // match
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil // no match
|
||||
}
|
||||
|
||||
func (a Any) Hash() string {
|
||||
var sb strings.Builder
|
||||
|
||||
for _, c := range a {
|
||||
fmt.Fprintln(&sb, c.Hash())
|
||||
}
|
||||
|
||||
return internal.FastHash(sb.String())
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MockChecker struct {
|
||||
Result bool
|
||||
Err error
|
||||
}
|
||||
|
||||
func (m MockChecker) Check(r *http.Request) (bool, error) {
|
||||
return m.Result, m.Err
|
||||
}
|
||||
|
||||
func (m MockChecker) Hash() string {
|
||||
return "mock-hash"
|
||||
}
|
||||
|
||||
func TestAny_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
checkers []MockChecker
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "All match",
|
||||
checkers: []MockChecker{
|
||||
{Result: true, Err: nil},
|
||||
{Result: true, Err: nil},
|
||||
},
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "One match",
|
||||
checkers: []MockChecker{
|
||||
{Result: false, Err: nil},
|
||||
{Result: true, Err: nil},
|
||||
},
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "No match",
|
||||
checkers: []MockChecker{
|
||||
{Result: false, Err: nil},
|
||||
{Result: false, Err: nil},
|
||||
},
|
||||
want: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Error encountered",
|
||||
checkers: []MockChecker{
|
||||
{Result: false, Err: nil},
|
||||
{Result: false, Err: http.ErrNotSupported},
|
||||
},
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var any Any
|
||||
for _, mc := range tt.checkers {
|
||||
any = append(any, mc)
|
||||
}
|
||||
|
||||
got, err := any.Check(nil)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Any.Check() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Any.Check() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Package checker defines the Checker interface and a helper utility to avoid import cycles.
|
||||
package checker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnparseableConfig = errors.New("checker: config is unparseable")
|
||||
ErrInvalidConfig = errors.New("checker: config is invalid")
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Check(*http.Request) (matches bool, err error)
|
||||
Hash() string
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
func TestBot(t *testing.T) {
|
||||
env, err := Bot()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create bot environment: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
headers map[string]string
|
||||
expected types.Bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "missing-header",
|
||||
expression: `missingHeader(headers, "Missing-Header")`,
|
||||
headers: map[string]string{
|
||||
"User-Agent": "test-agent",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: types.Bool(true),
|
||||
description: "should return true when header is missing",
|
||||
},
|
||||
{
|
||||
name: "existing-header",
|
||||
expression: `missingHeader(headers, "User-Agent")`,
|
||||
headers: map[string]string{
|
||||
"User-Agent": "test-agent",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: types.Bool(false),
|
||||
description: "should return false when header exists",
|
||||
},
|
||||
{
|
||||
name: "case-sensitive",
|
||||
expression: `missingHeader(headers, "user-agent")`,
|
||||
headers: map[string]string{
|
||||
"User-Agent": "test-agent",
|
||||
},
|
||||
expected: types.Bool(true),
|
||||
description: "should be case-sensitive (user-agent != User-Agent)",
|
||||
},
|
||||
{
|
||||
name: "empty-headers",
|
||||
expression: `missingHeader(headers, "Any-Header")`,
|
||||
headers: map[string]string{},
|
||||
expected: types.Bool(true),
|
||||
description: "should return true for any header when map is empty",
|
||||
},
|
||||
{
|
||||
name: "real-world-sec-ch-ua",
|
||||
expression: `missingHeader(headers, "Sec-Ch-Ua")`,
|
||||
headers: map[string]string{
|
||||
"User-Agent": "curl/7.68.0",
|
||||
"Accept": "*/*",
|
||||
"Host": "example.com",
|
||||
},
|
||||
expected: types.Bool(true),
|
||||
description: "should detect missing browser-specific headers from bots",
|
||||
},
|
||||
{
|
||||
name: "browser-with-sec-ch-ua",
|
||||
expression: `missingHeader(headers, "Sec-Ch-Ua")`,
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Sec-Ch-Ua": `"Chrome"; v="91", "Not A Brand"; v="99"`,
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
},
|
||||
expected: types.Bool(false),
|
||||
description: "should return false when browser sends Sec-Ch-Ua header",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prog, err := Compile(env, tt.expression)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(map[string]interface{}{
|
||||
"headers": tt.headers,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("function-compilation", func(t *testing.T) {
|
||||
src := `missingHeader(headers, "Test-Header")`
|
||||
_, err := Compile(env, src)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile missingHeader expression: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThreshold(t *testing.T) {
|
||||
env, err := Threshold()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create threshold environment: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
variables map[string]interface{}
|
||||
expected types.Bool
|
||||
description string
|
||||
shouldCompile bool
|
||||
}{
|
||||
{
|
||||
name: "weight-variable-available",
|
||||
expression: `weight > 100`,
|
||||
variables: map[string]interface{}{"weight": 150},
|
||||
expected: types.Bool(true),
|
||||
description: "should support weight variable in expressions",
|
||||
shouldCompile: true,
|
||||
},
|
||||
{
|
||||
name: "weight-variable-false-case",
|
||||
expression: `weight > 100`,
|
||||
variables: map[string]interface{}{"weight": 50},
|
||||
expected: types.Bool(false),
|
||||
description: "should correctly evaluate weight comparisons",
|
||||
shouldCompile: true,
|
||||
},
|
||||
{
|
||||
name: "missingHeader-not-available",
|
||||
expression: `missingHeader(headers, "Test")`,
|
||||
variables: map[string]interface{}{},
|
||||
expected: types.Bool(false), // not used
|
||||
description: "should not have missingHeader function available",
|
||||
shouldCompile: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prog, err := Compile(env, tt.expression)
|
||||
|
||||
if !tt.shouldCompile {
|
||||
if err == nil {
|
||||
t.Fatalf("%s: expected compilation to fail but it succeeded", tt.description)
|
||||
}
|
||||
return // Test passed - compilation failed as expected
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(tt.variables)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEnvironment(t *testing.T) {
|
||||
env, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create new environment: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
variables map[string]interface{}
|
||||
expectBool *bool // nil if we just want to test compilation or non-bool result
|
||||
description string
|
||||
shouldCompile bool
|
||||
}{
|
||||
{
|
||||
name: "randInt-function-compilation",
|
||||
expression: `randInt(10)`,
|
||||
variables: map[string]interface{}{},
|
||||
expectBool: nil, // Don't check result, just compilation
|
||||
description: "should compile randInt function",
|
||||
shouldCompile: true,
|
||||
},
|
||||
{
|
||||
name: "randInt-range-validation",
|
||||
expression: `randInt(10) >= 0 && randInt(10) < 10`,
|
||||
variables: map[string]interface{}{},
|
||||
expectBool: boolPtr(true),
|
||||
description: "should return values in correct range",
|
||||
shouldCompile: true,
|
||||
},
|
||||
{
|
||||
name: "strings-extension-size",
|
||||
expression: `"hello".size() == 5`,
|
||||
variables: map[string]interface{}{},
|
||||
expectBool: boolPtr(true),
|
||||
description: "should support string extension functions",
|
||||
shouldCompile: true,
|
||||
},
|
||||
{
|
||||
name: "strings-extension-contains",
|
||||
expression: `"hello world".contains("world")`,
|
||||
variables: map[string]interface{}{},
|
||||
expectBool: boolPtr(true),
|
||||
description: "should support string contains function",
|
||||
shouldCompile: true,
|
||||
},
|
||||
{
|
||||
name: "strings-extension-startsWith",
|
||||
expression: `"hello world".startsWith("hello")`,
|
||||
variables: map[string]interface{}{},
|
||||
expectBool: boolPtr(true),
|
||||
description: "should support string startsWith function",
|
||||
shouldCompile: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prog, err := Compile(env, tt.expression)
|
||||
|
||||
if !tt.shouldCompile {
|
||||
if err == nil {
|
||||
t.Fatalf("%s: expected compilation to fail but it succeeded", tt.description)
|
||||
}
|
||||
return // Test passed - compilation failed as expected
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
// If we only want to test compilation, skip evaluation
|
||||
if tt.expectBool == nil {
|
||||
return
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(tt.variables)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
if result != types.Bool(*tt.expectBool) {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, *tt.expectBool, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create bool pointers
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package expression
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
checker.Register("expression", Factory{})
|
||||
}
|
||||
|
||||
type Factory struct{}
|
||||
|
||||
func (f Factory) Build(ctx context.Context, data json.RawMessage) (checker.Interface, error) {
|
||||
var fc = &Config{}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), fc); err != nil {
|
||||
return nil, errors.Join(checker.ErrUnparseableConfig, err)
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return nil, errors.Join(checker.ErrInvalidConfig, err)
|
||||
}
|
||||
|
||||
return New(fc)
|
||||
}
|
||||
|
||||
func (f Factory) Valid(ctx context.Context, data json.RawMessage) error {
|
||||
var fc = &Config{}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), fc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package headerexists
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func New(key string) checker.Interface {
|
||||
return headerExistsChecker{
|
||||
header: strings.TrimSpace(http.CanonicalHeaderKey(key)),
|
||||
hash: internal.FastHash(key),
|
||||
}
|
||||
}
|
||||
|
||||
type headerExistsChecker struct {
|
||||
header, hash string
|
||||
}
|
||||
|
||||
func (hec headerExistsChecker) Check(r *http.Request) (bool, error) {
|
||||
if r.Header.Get(hec.header) != "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (hec headerExistsChecker) Hash() string {
|
||||
return hec.hash
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package headerexists
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChecker(t *testing.T) {
|
||||
fac := Factory{}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
header string
|
||||
reqHeader string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "match",
|
||||
header: "Authorization",
|
||||
reqHeader: "Authorization",
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "not_match",
|
||||
header: "Authorization",
|
||||
reqHeader: "Authentication",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hec, err := fac.Build(t.Context(), json.RawMessage(fmt.Sprintf("%q", tt.header)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log(hec.Hash())
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
r.Header.Set(tt.reqHeader, "hunter2")
|
||||
|
||||
ok, err := hec.Check(r)
|
||||
|
||||
if tt.ok != ok {
|
||||
t.Errorf("ok: %v, wanted: %v", ok, tt.ok)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("err: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package headerexists
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
type Factory struct{}
|
||||
|
||||
func (f Factory) Build(ctx context.Context, data json.RawMessage) (checker.Interface, error) {
|
||||
var headerName string
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &headerName); err != nil {
|
||||
return nil, fmt.Errorf("%w: want string", checker.ErrUnparseableConfig)
|
||||
}
|
||||
|
||||
if err := f.Valid(ctx, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return New(http.CanonicalHeaderKey(headerName)), nil
|
||||
}
|
||||
|
||||
func (Factory) Valid(ctx context.Context, data json.RawMessage) error {
|
||||
var headerName string
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &headerName); err != nil {
|
||||
return fmt.Errorf("%w: want string", checker.ErrUnparseableConfig)
|
||||
}
|
||||
|
||||
if headerName == "" {
|
||||
return fmt.Errorf("%w: string must not be empty", checker.ErrInvalidConfig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package headerexists
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFactoryGood(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/good")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "good", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFactoryBad(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/bad")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "bad", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("Build", func(t *testing.T) {
|
||||
if _, err := fac.Build(t.Context(), json.RawMessage(data)); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
""
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
"Authorization"
|
||||
@@ -1,46 +0,0 @@
|
||||
package headermatches
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
type Checker struct {
|
||||
header string
|
||||
regexp *regexp.Regexp
|
||||
hash string
|
||||
}
|
||||
|
||||
func (c *Checker) Check(r *http.Request) (bool, error) {
|
||||
if c.regexp.MatchString(r.Header.Get(c.header)) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *Checker) Hash() string {
|
||||
return c.hash
|
||||
}
|
||||
|
||||
func New(key, valueRex string) (checker.Interface, error) {
|
||||
fc := fileConfig{
|
||||
Header: key,
|
||||
ValueRegex: valueRex,
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(fc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Factory{}.Build(context.Background(), json.RawMessage(data))
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package headermatches
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChecker(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestHeaderMatchesChecker(t *testing.T) {
|
||||
fac := Factory{}
|
||||
|
||||
for _, tt := range []struct {
|
||||
err error
|
||||
name string
|
||||
header string
|
||||
rexStr string
|
||||
reqHeaderKey string
|
||||
reqHeaderValue string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "match",
|
||||
header: "Cf-Worker",
|
||||
rexStr: ".*",
|
||||
reqHeaderKey: "Cf-Worker",
|
||||
reqHeaderValue: "true",
|
||||
ok: true,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "not_match",
|
||||
header: "Cf-Worker",
|
||||
rexStr: "false",
|
||||
reqHeaderKey: "Cf-Worker",
|
||||
reqHeaderValue: "true",
|
||||
ok: false,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "not_present",
|
||||
header: "Cf-Worker",
|
||||
rexStr: "foobar",
|
||||
reqHeaderKey: "Something-Else",
|
||||
reqHeaderValue: "true",
|
||||
ok: false,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid_regex",
|
||||
rexStr: "a(b",
|
||||
err: ErrInvalidRegex,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fc := fileConfig{
|
||||
Header: tt.header,
|
||||
ValueRegex: tt.rexStr,
|
||||
}
|
||||
data, err := json.Marshal(fc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hmc, err := fac.Build(t.Context(), json.RawMessage(data))
|
||||
if err != nil && !errors.Is(err, tt.err) {
|
||||
t.Fatalf("creating HeaderMatchesChecker failed")
|
||||
}
|
||||
|
||||
if tt.err != nil && hmc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Log(hmc.Hash())
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
r.Header.Set(tt.reqHeaderKey, tt.reqHeaderValue)
|
||||
|
||||
ok, err := hmc.Check(r)
|
||||
|
||||
if tt.ok != ok {
|
||||
t.Errorf("ok: %v, wanted: %v", ok, tt.ok)
|
||||
}
|
||||
|
||||
if err != nil && tt.err != nil && !errors.Is(err, tt.err) {
|
||||
t.Errorf("err: %v, wanted: %v", err, tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package headermatches
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoHeader = errors.New("headermatches: no header is configured")
|
||||
ErrNoValueRegex = errors.New("headermatches: no value regex is configured")
|
||||
ErrInvalidRegex = errors.New("headermatches: value regex is invalid")
|
||||
)
|
||||
|
||||
type fileConfig struct {
|
||||
Header string `json:"header" yaml:"header"`
|
||||
ValueRegex string `json:"value_regex" yaml:"value_regex"`
|
||||
}
|
||||
|
||||
func (fc fileConfig) String() string {
|
||||
return fmt.Sprintf("header=%q value_regex=%q", fc.Header, fc.ValueRegex)
|
||||
}
|
||||
|
||||
func (fc fileConfig) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if fc.Header == "" {
|
||||
errs = append(errs, ErrNoHeader)
|
||||
}
|
||||
|
||||
if fc.ValueRegex == "" {
|
||||
errs = append(errs, ErrNoValueRegex)
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(fc.ValueRegex); err != nil {
|
||||
errs = append(errs, ErrInvalidRegex, err)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package headermatches
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileConfigValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name, description string
|
||||
in fileConfig
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "simple happy",
|
||||
description: "the most common usecase",
|
||||
in: fileConfig{
|
||||
Header: "User-Agent",
|
||||
ValueRegex: ".*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no header",
|
||||
description: "Header must be set, it is not",
|
||||
in: fileConfig{
|
||||
ValueRegex: ".*",
|
||||
},
|
||||
err: ErrNoHeader,
|
||||
},
|
||||
{
|
||||
name: "no value regex",
|
||||
description: "ValueRegex must be set, it is not",
|
||||
in: fileConfig{
|
||||
Header: "User-Agent",
|
||||
},
|
||||
err: ErrNoValueRegex,
|
||||
},
|
||||
{
|
||||
name: "invalid regex",
|
||||
description: "the user wrote an invalid value regular expression",
|
||||
in: fileConfig{
|
||||
Header: "User-Agent",
|
||||
ValueRegex: "[a-z",
|
||||
},
|
||||
err: ErrInvalidRegex,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.in.Valid(); !errors.Is(err, tt.err) {
|
||||
t.Log(tt.description)
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package headermatches
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
checker.Register("header_matches", Factory{})
|
||||
checker.Register("user_agent", Factory{defaultHeader: "User-Agent"})
|
||||
}
|
||||
|
||||
type Factory struct {
|
||||
defaultHeader string
|
||||
}
|
||||
|
||||
func (f Factory) Build(ctx context.Context, data json.RawMessage) (checker.Interface, error) {
|
||||
var fc fileConfig
|
||||
|
||||
if f.defaultHeader != "" {
|
||||
fc.Header = f.defaultHeader
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &fc); err != nil {
|
||||
return nil, errors.Join(checker.ErrUnparseableConfig, err)
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return nil, errors.Join(checker.ErrInvalidConfig, err)
|
||||
}
|
||||
|
||||
valueRex, err := regexp.Compile(fc.ValueRegex)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrInvalidRegex, err)
|
||||
}
|
||||
|
||||
return &Checker{
|
||||
header: http.CanonicalHeaderKey(fc.Header),
|
||||
regexp: valueRex,
|
||||
hash: internal.FastHash(fc.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f Factory) Valid(ctx context.Context, data json.RawMessage) error {
|
||||
var fc fileConfig
|
||||
|
||||
if f.defaultHeader != "" {
|
||||
fc.Header = f.defaultHeader
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &fc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package headermatches
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFactoryGood(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/good")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "good", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFactoryBad(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/bad")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "bad", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"header": "User-Agent",
|
||||
"value_regex": "a(b"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"value_regex": "PaleMoon"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"header": "User-Agent"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"header": "User-Agent",
|
||||
"value_regex": "PaleMoon"
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package headermatches
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func ValidUserAgent(valueRex string) error {
|
||||
fc := fileConfig{
|
||||
Header: "User-Agent",
|
||||
ValueRegex: valueRex,
|
||||
}
|
||||
|
||||
return fc.Valid()
|
||||
}
|
||||
|
||||
func NewUserAgent(valueRex string) (checker.Interface, error) {
|
||||
fc := fileConfig{
|
||||
Header: "User-Agent",
|
||||
ValueRegex: valueRex,
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(fc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Factory{}.Build(context.Background(), json.RawMessage(data))
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func New(rexStr string) (checker.Interface, error) {
|
||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: regex %s failed parse: %w", anubis.ErrMisconfiguration, rexStr, err)
|
||||
}
|
||||
return &Checker{rex, internal.FastHash(rexStr)}, nil
|
||||
}
|
||||
|
||||
type Checker struct {
|
||||
regexp *regexp.Regexp
|
||||
hash string
|
||||
}
|
||||
|
||||
func (c *Checker) Check(r *http.Request) (bool, error) {
|
||||
if c.regexp.MatchString(r.URL.Path) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *Checker) Hash() string {
|
||||
return c.hash
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChecker(t *testing.T) {
|
||||
fac := Factory{}
|
||||
|
||||
for _, tt := range []struct {
|
||||
err error
|
||||
name string
|
||||
rexStr string
|
||||
reqPath string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "match",
|
||||
rexStr: "^/api/.*",
|
||||
reqPath: "/api/v1/users",
|
||||
ok: true,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "not_match",
|
||||
rexStr: "^/api/.*",
|
||||
reqPath: "/static/index.html",
|
||||
ok: false,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "wildcard_match",
|
||||
rexStr: ".*\\.json$",
|
||||
reqPath: "/data/config.json",
|
||||
ok: true,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "wildcard_not_match",
|
||||
rexStr: ".*\\.json$",
|
||||
reqPath: "/data/config.yaml",
|
||||
ok: false,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid_regex",
|
||||
rexStr: "a(b",
|
||||
err: ErrInvalidRegex,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fc := fileConfig{
|
||||
Regex: tt.rexStr,
|
||||
}
|
||||
data, err := json.Marshal(fc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pc, err := fac.Build(t.Context(), json.RawMessage(data))
|
||||
if err != nil && !errors.Is(err, tt.err) {
|
||||
t.Fatalf("creating PathChecker failed")
|
||||
}
|
||||
|
||||
if tt.err != nil && pc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Log(pc.Hash())
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, tt.reqPath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
ok, err := pc.Check(r)
|
||||
|
||||
if tt.ok != ok {
|
||||
t.Errorf("ok: %v, wanted: %v", ok, tt.ok)
|
||||
}
|
||||
|
||||
if err != nil && tt.err != nil && !errors.Is(err, tt.err) {
|
||||
t.Errorf("err: %v, wanted: %v", err, tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoRegex = errors.New("path: no regex is configured")
|
||||
ErrInvalidRegex = errors.New("path: regex is invalid")
|
||||
)
|
||||
|
||||
type fileConfig struct {
|
||||
Regex string `json:"regex" yaml:"regex"`
|
||||
}
|
||||
|
||||
func (fc fileConfig) String() string {
|
||||
return fmt.Sprintf("regex=%q", fc.Regex)
|
||||
}
|
||||
|
||||
func (fc fileConfig) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if fc.Regex == "" {
|
||||
errs = append(errs, ErrNoRegex)
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(fc.Regex); err != nil {
|
||||
errs = append(errs, ErrInvalidRegex, err)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileConfigValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name, description string
|
||||
in fileConfig
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "simple happy",
|
||||
description: "the most common usecase",
|
||||
in: fileConfig{
|
||||
Regex: "^/api/.*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard match",
|
||||
description: "match files with specific extension",
|
||||
in: fileConfig{
|
||||
Regex: ".*[.]json$",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no regex",
|
||||
description: "Regex must be set, it is not",
|
||||
in: fileConfig{},
|
||||
err: ErrNoRegex,
|
||||
},
|
||||
{
|
||||
name: "invalid regex",
|
||||
description: "the user wrote an invalid regular expression",
|
||||
in: fileConfig{
|
||||
Regex: "[a-z",
|
||||
},
|
||||
err: ErrInvalidRegex,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.in.Valid(); !errors.Is(err, tt.err) {
|
||||
t.Log(tt.description)
|
||||
t.Fatalf("got %v, wanted %v", err, tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
checker.Register("path", Factory{})
|
||||
}
|
||||
|
||||
type Factory struct{}
|
||||
|
||||
func (f Factory) Build(ctx context.Context, data json.RawMessage) (checker.Interface, error) {
|
||||
var fc fileConfig
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &fc); err != nil {
|
||||
return nil, errors.Join(checker.ErrUnparseableConfig, err)
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return nil, errors.Join(checker.ErrInvalidConfig, err)
|
||||
}
|
||||
|
||||
pathRex, err := regexp.Compile(strings.TrimSpace(fc.Regex))
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrInvalidRegex, err)
|
||||
}
|
||||
|
||||
return &Checker{
|
||||
regexp: pathRex,
|
||||
hash: internal.FastHash(fc.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f Factory) Valid(ctx context.Context, data json.RawMessage) error {
|
||||
var fc fileConfig
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &fc); err != nil {
|
||||
return errors.Join(checker.ErrUnparseableConfig, err)
|
||||
}
|
||||
|
||||
return fc.Valid()
|
||||
}
|
||||
|
||||
func Valid(pathRex string) error {
|
||||
fc := fileConfig{
|
||||
Regex: pathRex,
|
||||
}
|
||||
|
||||
return fc.Valid()
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFactoryGood(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/good")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "good", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFactoryBad(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/bad")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "bad", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err == nil {
|
||||
t.Fatal("expected validation to fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"regex": "a(b"
|
||||
}
|
||||
1
lib/checker/path/testdata/bad/nothing.json
vendored
1
lib/checker/path/testdata/bad/nothing.json
vendored
@@ -1 +0,0 @@
|
||||
{}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user