mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-05 08:18:17 +00:00
Compare commits
45 Commits
v1.20.0-pr
...
Xe/devcont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa8c45c989 | ||
|
|
93bfe910d8 | ||
|
|
19d8de784b | ||
|
|
dff2176beb | ||
|
|
506d8817d5 | ||
|
|
d0fae02d05 | ||
|
|
845095c3f6 | ||
|
|
2f1e78cc6c | ||
|
|
7c0996448a | ||
|
|
d7a758f805 | ||
|
|
c121896f9c | ||
|
|
888b7d6e77 | ||
|
|
0e43138324 | ||
|
|
c981c23f7e | ||
|
|
9f0c5e974e | ||
|
|
292c470ada | ||
|
|
12453fdc00 | ||
|
|
f5b3bf81bc | ||
|
|
1820649987 | ||
|
|
14eeeb56d6 | ||
|
|
d9e0fbe905 | ||
|
|
6aa17532da | ||
|
|
b1edf84a7c | ||
|
|
d47a3406db | ||
|
|
ff5991b5cf | ||
|
|
19f78f37ad | ||
|
|
b0b0a5c08a | ||
|
|
261306dc63 | ||
|
|
3520421757 | ||
|
|
ad5430612f | ||
|
|
c2423d0688 | ||
|
|
a1b7d2ccda | ||
|
|
7cf6ac5de6 | ||
|
|
59f5b07281 | ||
|
|
1562f88c35 | ||
|
|
15bd9b6a44 | ||
|
|
1ca531b930 | ||
|
|
f9259299b9 | ||
|
|
16a4e04027 | ||
|
|
8c79870edb | ||
|
|
060b10ea2d | ||
|
|
4c74934e9f | ||
|
|
5870f7072c | ||
|
|
3c1d95d61e | ||
|
|
ab801a3597 |
12
.devcontainer/Dockerfile
Normal file
12
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM ghcr.io/xe/devcontainer-base/pre/go
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum package.json package-lock.json ./
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install zstd brotli redis \
|
||||
&& mkdir -p /home/vscode/.local/share/fish \
|
||||
&& chown -R vscode:vscode /home/vscode/.local/share/fish \
|
||||
&& chown -R vscode:vscode /go
|
||||
|
||||
CMD ["/usr/bin/sleep", "infinity"]
|
||||
13
.devcontainer/README.md
Normal file
13
.devcontainer/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Anubis Dev Container
|
||||
|
||||
Anubis offers a [development container](https://containers.dev/) image in order to make it easier to contribute to the project. This image is based on [Xe/devcontainer-base/go](https://github.com/Xe/devcontainer-base/tree/main/src/go), which is based on Debian Bookworm with the following customizations:
|
||||
|
||||
- [Fish](https://fishshell.com/) as the shell complete with a custom theme
|
||||
- [Go](https://go.dev) at the most recent stable version
|
||||
- [Node.js](https://nodejs.org/en) at the most recent stable version
|
||||
- [Atuin](https://atuin.sh/) to sync shell history between your host OS and the development container
|
||||
- [Docker](https://docker.com) to manage and build Anubis container images from inside the development container
|
||||
- [Ko](https://ko.build/) to build production-ready Anubis container images
|
||||
- [Neovim](https://neovim.io/) for use with Git
|
||||
|
||||
This development container is tested and known to work with [Visual Studio Code](https://code.visualstudio.com/). If you run into problems with it outside of VS Code, please file an issue and let us know what editor you are using.
|
||||
25
.devcontainer/devcontainer.json
Normal file
25
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,25 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
|
||||
{
|
||||
"name": "Dev",
|
||||
"dockerComposeFile": ["./docker-compose.yaml"],
|
||||
"service": "workspace",
|
||||
"workspaceFolder": "/workspace/anubis",
|
||||
"postStartCommand": "npm ci && go mod download",
|
||||
"features": {
|
||||
"ghcr.io/xe/devcontainer-features/ko:1.1.0": {}
|
||||
},
|
||||
"initializeCommand": "mkdir -p ${localEnv:HOME}${localEnv:USERPROFILE}/.local/share/atuin",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-azuretools.vscode-containers",
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
26
.devcontainer/docker-compose.yaml
Normal file
26
.devcontainer/docker-compose.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
playwright:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-noble
|
||||
init: true
|
||||
network_mode: service:workspace
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- npx -y playwright@1.52.0 run-server --port 9001 --host 0.0.0.0
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:8
|
||||
pull_policy: always
|
||||
|
||||
# VS Code workspace service
|
||||
workspace:
|
||||
image: ghcr.io/techarohq/anubis/devcontainer
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
volumes:
|
||||
- ../:/workspace/anubis:cached
|
||||
environment:
|
||||
VALKEY_URL: redis://valkey:6379/0
|
||||
#entrypoint: ["/usr/bin/sleep", "infinity"]
|
||||
user: vscode
|
||||
2
.github/actions/spelling/excludes.txt
vendored
2
.github/actions/spelling/excludes.txt
vendored
@@ -89,3 +89,5 @@
|
||||
^lib/policy/config/testdata/bad/unparseable\.json$
|
||||
ignore$
|
||||
robots.txt
|
||||
^lib/localization/locales/.*\.json$
|
||||
^lib/localization/.*_test.go$
|
||||
|
||||
39
.github/actions/spelling/expect.txt
vendored
39
.github/actions/spelling/expect.txt
vendored
@@ -13,16 +13,21 @@ asnc
|
||||
asnchecker
|
||||
asns
|
||||
aspirational
|
||||
atuin
|
||||
azuretools
|
||||
badregexes
|
||||
bbolt
|
||||
bdba
|
||||
berr
|
||||
bingbot
|
||||
bitcoin
|
||||
bitrate
|
||||
blogging
|
||||
Bluesky
|
||||
blueskybot
|
||||
boi
|
||||
botnet
|
||||
botstopper
|
||||
BPort
|
||||
Brightbot
|
||||
broked
|
||||
@@ -33,7 +38,7 @@ Caddyfile
|
||||
caninetools
|
||||
Cardyb
|
||||
celchecker
|
||||
CELPHASE
|
||||
celphase
|
||||
cerr
|
||||
certresolver
|
||||
cespare
|
||||
@@ -42,17 +47,22 @@ cgr
|
||||
chainguard
|
||||
chall
|
||||
challengemozilla
|
||||
challengetest
|
||||
checkpath
|
||||
checkresult
|
||||
chibi
|
||||
cidranger
|
||||
ckie
|
||||
ckies
|
||||
cloudflare
|
||||
Codespaces
|
||||
confd
|
||||
connnection
|
||||
containerbuild
|
||||
coreutils
|
||||
Cotoyogi
|
||||
CRDs
|
||||
Cromite
|
||||
crt
|
||||
Cscript
|
||||
daemonizing
|
||||
@@ -67,13 +77,17 @@ distros
|
||||
dnf
|
||||
dnsbl
|
||||
dnserr
|
||||
domainhere
|
||||
dracula
|
||||
dronebl
|
||||
droneblresponse
|
||||
dropin
|
||||
duckduckbot
|
||||
eerror
|
||||
ellenjoe
|
||||
emacs
|
||||
enbyware
|
||||
etld
|
||||
everyones
|
||||
evilbot
|
||||
evilsite
|
||||
@@ -85,6 +99,7 @@ facebookgo
|
||||
Factset
|
||||
fastcgi
|
||||
fediverse
|
||||
ffprobe
|
||||
finfos
|
||||
Firecrawl
|
||||
flagenv
|
||||
@@ -129,6 +144,7 @@ iat
|
||||
ifm
|
||||
Imagesift
|
||||
imgproxy
|
||||
impressum
|
||||
inp
|
||||
IPTo
|
||||
iptoasn
|
||||
@@ -144,6 +160,7 @@ JWTs
|
||||
kagi
|
||||
kagibot
|
||||
keikaku
|
||||
Keyfunc
|
||||
keypair
|
||||
KHTML
|
||||
kinda
|
||||
@@ -178,17 +195,18 @@ mozilla
|
||||
nbf
|
||||
netsurf
|
||||
nginx
|
||||
nicksnyder
|
||||
nobots
|
||||
NONINFRINGEMENT
|
||||
nosleep
|
||||
OCOB
|
||||
ogtags
|
||||
ogtitle
|
||||
omgili
|
||||
omgilibot
|
||||
openai
|
||||
opengraph
|
||||
openrc
|
||||
oswald
|
||||
pag
|
||||
palemoon
|
||||
Pangu
|
||||
@@ -208,6 +226,7 @@ privkey
|
||||
promauto
|
||||
promhttp
|
||||
proofofwork
|
||||
publicsuffix
|
||||
pwcmd
|
||||
pwuser
|
||||
qualys
|
||||
@@ -216,8 +235,11 @@ qwantbot
|
||||
rac
|
||||
rawler
|
||||
rcvar
|
||||
rdb
|
||||
redhat
|
||||
redir
|
||||
redirectscheme
|
||||
refactors
|
||||
relayd
|
||||
reputational
|
||||
reqmeta
|
||||
@@ -225,6 +247,7 @@ risc
|
||||
ruleset
|
||||
runlevels
|
||||
RUnlock
|
||||
runtimedir
|
||||
sas
|
||||
sasl
|
||||
Scumm
|
||||
@@ -238,7 +261,9 @@ Seo
|
||||
setsebool
|
||||
shellcheck
|
||||
Sidetrade
|
||||
simprint
|
||||
sitemap
|
||||
skopeo
|
||||
sls
|
||||
sni
|
||||
Sourceware
|
||||
@@ -249,6 +274,7 @@ srv
|
||||
stackoverflow
|
||||
startprecmd
|
||||
stoppostcmd
|
||||
storetest
|
||||
subgrid
|
||||
subr
|
||||
subrequest
|
||||
@@ -256,23 +282,31 @@ SVCNAME
|
||||
tagline
|
||||
tarballs
|
||||
tarrif
|
||||
tbn
|
||||
tbr
|
||||
techaro
|
||||
techarohq
|
||||
templ
|
||||
templruntime
|
||||
testarea
|
||||
testdb
|
||||
Thancred
|
||||
thoth
|
||||
thothmock
|
||||
Tik
|
||||
Timpibot
|
||||
traefik
|
||||
uberspace
|
||||
Unbreak
|
||||
unbreakdocker
|
||||
unifiedjs
|
||||
unixhttpd
|
||||
unmarshal
|
||||
unparseable
|
||||
uuidgen
|
||||
uvx
|
||||
UXP
|
||||
valkey
|
||||
Varis
|
||||
Velen
|
||||
vendored
|
||||
@@ -310,4 +344,5 @@ yourdomain
|
||||
yoursite
|
||||
Zenos
|
||||
zizmor
|
||||
zombocom
|
||||
zos
|
||||
|
||||
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
|
||||
6
.github/workflows/docs-deploy.yml
vendored
6
.github/workflows/docs-deploy.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
@@ -50,14 +50,14 @@ jobs:
|
||||
push: true
|
||||
|
||||
- name: Apply k8s manifests to aeacus
|
||||
uses: actions-hub/kubectl@f632a31512a74cb35940627c49c20f67723cbaaf # v1.33.1
|
||||
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 aeacus
|
||||
uses: actions-hub/kubectl@f632a31512a74cb35940627c49c20f67723cbaaf # v1.33.1
|
||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/docs-test.yml
vendored
2
.github/workflows/docs-test.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
||||
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
sudo apt-get install -y build-essential
|
||||
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
|
||||
2
.github/workflows/package-builds-stable.yml
vendored
2
.github/workflows/package-builds-stable.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
sudo apt-get install -y build-essential
|
||||
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
sudo apt-get install -y build-essential
|
||||
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
|
||||
2
.github/workflows/ssh-ci-runner-cron.yml
vendored
2
.github/workflows/ssh-ci-runner-cron.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Build and push
|
||||
run: |
|
||||
cd ./test/ssh-ci
|
||||
|
||||
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@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
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@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/upload-sarif@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -20,3 +20,5 @@ node_modules
|
||||
|
||||
# how does this get here
|
||||
doc/VERSION
|
||||
|
||||
web/static/locales/*.json
|
||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-azuretools.vscode-containers",
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
27
.vscode/launch.json
vendored
Normal file
27
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${fileDirname}"
|
||||
},
|
||||
{
|
||||
"name": "Anubis [dev]",
|
||||
"command": "npm run dev",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"name": "Start Docs",
|
||||
"command": "cd docs && npm ci && npm run start",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
Makefile
2
Makefile
@@ -18,6 +18,7 @@ assets: deps
|
||||
|
||||
build: assets
|
||||
$(GO) build -o ./var/anubis ./cmd/anubis
|
||||
$(GO) build -o ./var/robots2policy ./cmd/robots2policy
|
||||
@echo "Anubis is now built to ./var/anubis"
|
||||
|
||||
lint: assets
|
||||
@@ -27,6 +28,7 @@ lint: assets
|
||||
|
||||
prebaked-build:
|
||||
$(GO) build -o ./var/anubis -ldflags "-X 'github.com/TecharoHQ/anubis.Version=$(VERSION)'" ./cmd/anubis
|
||||
$(GO) build -o ./var/robots2policy -ldflags "-X 'github.com/TecharoHQ/anubis.Version=$(VERSION)'" ./cmd/robots2policy
|
||||
|
||||
test: assets
|
||||
$(GO) test ./...
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||

|
||||

|
||||

|
||||
[](https://github.com/sponsors/Xe)
|
||||
|
||||
## Sponsors
|
||||
|
||||
|
||||
13
anubis.go
13
anubis.go
@@ -11,12 +11,11 @@ var Version = "devel"
|
||||
|
||||
// CookieName is the name of the cookie that Anubis uses in order to validate
|
||||
// access.
|
||||
const CookieName = "techaro.lol-anubis-auth"
|
||||
var CookieName = "techaro.lol-anubis-auth"
|
||||
|
||||
// WithDomainCookieName is the name that is prepended to the per-domain cookie used when COOKIE_DOMAIN is set.
|
||||
const WithDomainCookieName = "techaro.lol-anubis-auth-for-"
|
||||
|
||||
const TestCookieName = "techaro.lol-anubis-cookie-test-if-you-block-this-anubis-wont-work"
|
||||
// TestCookieName is the name of the cookie that Anubis uses in order to check
|
||||
// if cookies are enabled on the client's browser.
|
||||
var TestCookieName = "techaro.lol-anubis-cookie-verification"
|
||||
|
||||
// CookieDefaultExpirationTime is the amount of time before the cookie/JWT expires.
|
||||
const CookieDefaultExpirationTime = 7 * 24 * time.Hour
|
||||
@@ -33,3 +32,7 @@ const APIPrefix = "/.within.website/x/cmd/anubis/api/"
|
||||
// DefaultDifficulty is the default "difficulty" (number of leading zeroes)
|
||||
// that must be met by the client in order to pass the challenge.
|
||||
const DefaultDifficulty = 4
|
||||
|
||||
// ForcedLanguage is the language being used instead of the one of the request's Accept-Language header
|
||||
// if being set.
|
||||
var ForcedLanguage = ""
|
||||
|
||||
@@ -46,8 +46,13 @@ var (
|
||||
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
||||
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
||||
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
|
||||
cookieDynamicDomain = flag.Bool("cookie-dynamic-domain", false, "if set, automatically set the cookie Domain value based on the request domain")
|
||||
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
|
||||
cookiePrefix = flag.String("cookie-prefix", "techaro.lol-anubis", "prefix for browser cookies created by Anubis")
|
||||
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
||||
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
|
||||
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
|
||||
cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
|
||||
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
||||
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
||||
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
||||
@@ -105,8 +110,41 @@ func doHealthCheck() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseBindNetFromAddr determine bind network and address based on the given network and address.
|
||||
func parseBindNetFromAddr(address string) (string, string) {
|
||||
defaultScheme := "http://"
|
||||
if !strings.Contains(address, "://") {
|
||||
if strings.HasPrefix(address, ":") {
|
||||
address = defaultScheme + "localhost" + address
|
||||
} else {
|
||||
address = defaultScheme + address
|
||||
}
|
||||
}
|
||||
|
||||
bindUri, err := url.Parse(address)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("failed to parse bind URL: %w", err))
|
||||
}
|
||||
|
||||
switch bindUri.Scheme {
|
||||
case "unix":
|
||||
return "unix", bindUri.Path
|
||||
case "tcp", "http", "https":
|
||||
return "tcp", bindUri.Host
|
||||
default:
|
||||
log.Fatal(fmt.Errorf("unsupported network scheme %s in address %s", bindUri.Scheme, address))
|
||||
}
|
||||
return "", address
|
||||
}
|
||||
|
||||
func setupListener(network string, address string) (net.Listener, string) {
|
||||
formattedAddress := ""
|
||||
|
||||
if network == "" {
|
||||
// keep compatibility
|
||||
network, address = parseBindNetFromAddr(address)
|
||||
}
|
||||
|
||||
switch network {
|
||||
case "unix":
|
||||
formattedAddress = "unix:" + address
|
||||
@@ -239,6 +277,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if *cookieDomain != "" && *cookieDynamicDomain {
|
||||
log.Fatalf("you can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN at the same time")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Thoth configuration
|
||||
@@ -290,11 +332,15 @@ func main() {
|
||||
"this may result in unexpected behavior")
|
||||
}
|
||||
|
||||
var priv ed25519.PrivateKey
|
||||
if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
|
||||
var ed25519Priv ed25519.PrivateKey
|
||||
if *hs512Secret != "" && (*ed25519PrivateKeyHex != "" || *ed25519PrivateKeyHexFile != "") {
|
||||
log.Fatal("do not specify both HS512 and ED25519 secrets")
|
||||
} else if *hs512Secret != "" {
|
||||
ed25519Priv = ed25519.PrivateKey(*hs512Secret)
|
||||
} else if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
|
||||
log.Fatal("do not specify both ED25519_PRIVATE_KEY_HEX and ED25519_PRIVATE_KEY_HEX_FILE")
|
||||
} else if *ed25519PrivateKeyHex != "" {
|
||||
priv, err = keyFromHex(*ed25519PrivateKeyHex)
|
||||
ed25519Priv, err = keyFromHex(*ed25519PrivateKeyHex)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err)
|
||||
}
|
||||
@@ -304,12 +350,12 @@ func main() {
|
||||
log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err)
|
||||
}
|
||||
|
||||
priv, err = keyFromHex(string(bytes.TrimSpace(hexFile)))
|
||||
ed25519Priv, err = keyFromHex(string(bytes.TrimSpace(hexFile)))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err)
|
||||
}
|
||||
} else {
|
||||
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
_, ed25519Priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate ed25519 key: %v", err)
|
||||
}
|
||||
@@ -331,6 +377,10 @@ func main() {
|
||||
slog.Warn("REDIRECT_DOMAINS is not set, Anubis will only redirect to the same domain a request is coming from, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains")
|
||||
}
|
||||
|
||||
anubis.CookieName = *cookiePrefix + "-auth"
|
||||
anubis.TestCookieName = *cookiePrefix + "-cookie-verification"
|
||||
anubis.ForcedLanguage = *forcedLanguage
|
||||
|
||||
// If OpenGraph configuration values are not set in the config file, use the
|
||||
// values from flags / envvars.
|
||||
if !policy.OpenGraph.Enabled {
|
||||
@@ -341,18 +391,22 @@ func main() {
|
||||
}
|
||||
|
||||
s, err := libanubis.New(libanubis.Options{
|
||||
BasePrefix: *basePrefix,
|
||||
StripBasePrefix: *stripBasePrefix,
|
||||
Next: rp,
|
||||
Policy: policy,
|
||||
ServeRobotsTXT: *robotsTxt,
|
||||
PrivateKey: priv,
|
||||
CookieDomain: *cookieDomain,
|
||||
CookieExpiration: *cookieExpiration,
|
||||
CookiePartitioned: *cookiePartitioned,
|
||||
RedirectDomains: redirectDomainsList,
|
||||
Target: *target,
|
||||
WebmasterEmail: *webmasterEmail,
|
||||
BasePrefix: *basePrefix,
|
||||
StripBasePrefix: *stripBasePrefix,
|
||||
Next: rp,
|
||||
Policy: policy,
|
||||
ServeRobotsTXT: *robotsTxt,
|
||||
ED25519PrivateKey: ed25519Priv,
|
||||
HS512Secret: []byte(*hs512Secret),
|
||||
CookieDomain: *cookieDomain,
|
||||
CookieDynamicDomain: *cookieDynamicDomain,
|
||||
CookieExpiration: *cookieExpiration,
|
||||
CookiePartitioned: *cookiePartitioned,
|
||||
RedirectDomains: redirectDomainsList,
|
||||
Target: *target,
|
||||
WebmasterEmail: *webmasterEmail,
|
||||
OpenGraph: policy.OpenGraph,
|
||||
CookieSecure: *cookieSecure,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
|
||||
@@ -84,6 +84,35 @@ bots:
|
||||
|
||||
dnsbl: false
|
||||
|
||||
# #
|
||||
# impressum:
|
||||
# # Displayed at the bottom of every page rendered by Anubis.
|
||||
# footer: >-
|
||||
# This website is hosted by Zombocom. If you have any complaints or notes
|
||||
# about the service, please contact
|
||||
# <a href="mailto:contact@domainhere.example">contact@domainhere.example</a>
|
||||
# and we will assist you as soon as possible.
|
||||
|
||||
# # The imprint page that will be linked to at the footer of every Anubis page.
|
||||
# page:
|
||||
# # The HTML <title> of the page
|
||||
# title: Imprint and Privacy Policy
|
||||
# # The HTML contents of the page. The exact contents of this page can
|
||||
# # and will vary by locale. Please consult with a lawyer if you are not
|
||||
# # sure what to put here
|
||||
# body: >-
|
||||
# <p>Last updated: June 2025</p>
|
||||
|
||||
# <h2>Information that is gathered from visitors</h2>
|
||||
|
||||
# <p>In common with other websites, log files are stored on the web server saving details such as the visitor's IP address, browser type, referring page and time of visit.</p>
|
||||
|
||||
# <p>Cookies may be used to remember visitor preferences when interacting with the website.</p>
|
||||
|
||||
# <p>Where registration is required, the visitor's email and a username will be stored on the server.</p>
|
||||
|
||||
# <!-- ... -->
|
||||
|
||||
# Open Graph passthrough configuration, see here for more information:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/open-graph/
|
||||
openGraph:
|
||||
@@ -116,6 +145,14 @@ status_codes:
|
||||
CHALLENGE: 200
|
||||
DENY: 200
|
||||
|
||||
# Anubis can store temporary data in one of a few backends. See the storage
|
||||
# backends section of the docs for more information:
|
||||
#
|
||||
# https://anubis.techaro.lol/docs/admin/policies#storage-backends
|
||||
store:
|
||||
backend: memory
|
||||
parameters: {}
|
||||
|
||||
# The weight thresholds for when to trigger individual challenges. Any
|
||||
# CHALLENGE will take precedence over this.
|
||||
#
|
||||
@@ -132,14 +169,14 @@ status_codes:
|
||||
thresholds:
|
||||
# By default Anubis ships with the following thresholds:
|
||||
- name: minimal-suspicion # This client is likely fine, its soul is lighter than a feather
|
||||
expression: weight < 0 # a feather weighs zero units
|
||||
expression: weight <= 0 # a feather weighs zero units
|
||||
action: ALLOW # Allow the traffic through
|
||||
# For clients that had some weight reduced through custom rules, give them a
|
||||
# lightweight challenge.
|
||||
- name: mild-suspicion
|
||||
expression:
|
||||
all:
|
||||
- weight >= 0
|
||||
- weight > 0
|
||||
- weight < 10
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
# Warning: May contain user agents that _must_ be blocked in robots.txt, or the opt-out will have no effect.
|
||||
- name: "ai-catchall"
|
||||
user_agent_regex: >-
|
||||
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Brightbot 1.0|Bytespider|CCBot|Claude-Web|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|GoogleOther|GoogleOther-Image|GoogleOther-Video|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|NovaAct|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|QualifiedBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YouBot
|
||||
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|anthropic-ai|Brightbot 1.0|Bytespider|Claude-Web|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|FacebookBot|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|GoogleOther|GoogleOther-Image|GoogleOther-Video|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|imgproxy|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|NovaAct|omgili|omgilibot|Operator|PanguBot|Perplexity-User|PerplexityBot|PetalBot|QualifiedBot|Scrapy|SemrushBot-OCOB|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YouBot
|
||||
action: DENY
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Warning: Contains user agents that _must_ be blocked in robots.txt, or the opt-out will have no effect.
|
||||
# Note: Blocks human-directed/non-training user agents
|
||||
#
|
||||
# CCBot is allowed because if Common Crawl is allowed, then scrapers don't need to scrape to get the data.
|
||||
- name: "ai-robots-txt"
|
||||
user_agent_regex: >-
|
||||
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|Andibot|anthropic-ai|Applebot|Applebot-Extended|bedrockbot|Brightbot 1.0|Bytespider|CCBot|ChatGPT-User|Claude-SearchBot|Claude-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|EchoboxBot|FacebookBot|facebookexternalhit|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|MistralAI-User/1.0|MyCentralAIScraperBot|NovaAct|OAI-SearchBot|omgili|omgilibot|Operator|PanguBot|Panscient|panscient.com|Perplexity-User|PerplexityBot|PetalBot|PhindBot|Poseidon Research Crawler|QualifiedBot|QuillBot|quillbot.com|SBIntuitionsBot|Scrapy|SemrushBot|SemrushBot-BA|SemrushBot-CT|SemrushBot-OCOB|SemrushBot-SI|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YandexAdditional|YandexAdditionalBot|YouBot
|
||||
AI2Bot|Ai2Bot-Dolma|aiHitBot|Amazonbot|Andibot|anthropic-ai|Applebot|Applebot-Extended|bedrockbot|Brightbot 1.0|Bytespider|ChatGPT-User|Claude-SearchBot|Claude-User|Claude-Web|ClaudeBot|cohere-ai|cohere-training-data-crawler|Cotoyogi|Crawlspace|Diffbot|DuckAssistBot|EchoboxBot|FacebookBot|facebookexternalhit|Factset_spyderbot|FirecrawlAgent|FriendlyCrawler|Google-CloudVertexBot|Google-Extended|GoogleOther|GoogleOther-Image|GoogleOther-Video|GPTBot|iaskspider/2.0|ICC-Crawler|ImagesiftBot|img2dataset|ISSCyberRiskCrawler|Kangaroo Bot|meta-externalagent|Meta-ExternalAgent|meta-externalfetcher|Meta-ExternalFetcher|MistralAI-User/1.0|MyCentralAIScraperBot|NovaAct|OAI-SearchBot|omgili|omgilibot|Operator|PanguBot|Panscient|panscient.com|Perplexity-User|PerplexityBot|PetalBot|PhindBot|Poseidon Research Crawler|QualifiedBot|QuillBot|quillbot.com|SBIntuitionsBot|Scrapy|SemrushBot|SemrushBot-BA|SemrushBot-CT|SemrushBot-OCOB|SemrushBot-SI|SemrushBot-SWA|Sidetrade indexer bot|TikTokSpider|Timpibot|VelenPublicWebCrawler|Webzio-Extended|wpbot|YandexAdditional|YandexAdditionalBot|YouBot
|
||||
action: DENY
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
- import: (data)/crawlers/internet-archive.yaml
|
||||
- import: (data)/crawlers/kagibot.yaml
|
||||
- import: (data)/crawlers/marginalia.yaml
|
||||
- import: (data)/crawlers/mojeekbot.yaml
|
||||
- import: (data)/crawlers/mojeekbot.yaml
|
||||
- import: (data)/crawlers/commoncrawl.yaml
|
||||
|
||||
12
data/crawlers/commoncrawl.yaml
Normal file
12
data/crawlers/commoncrawl.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
- name: common-crawl
|
||||
user_agent_regex: CCBot
|
||||
action: ALLOW
|
||||
# https://index.commoncrawl.org/ccbot.json
|
||||
remote_addresses:
|
||||
[
|
||||
"2600:1f28:365:80b0::/60",
|
||||
"18.97.9.168/29",
|
||||
"18.97.14.80/29",
|
||||
"18.97.14.88/30",
|
||||
"98.85.178.216/32",
|
||||
]
|
||||
@@ -48,6 +48,26 @@ func (m *Impl[K, V]) expire(key K) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Delete a value from the DecayMap by key.
|
||||
//
|
||||
// If the value does not exist, return false. Return true after
|
||||
// deletion.
|
||||
func (m *Impl[K, V]) Delete(key K) bool {
|
||||
m.lock.RLock()
|
||||
_, ok := m.data[key]
|
||||
m.lock.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
m.lock.Lock()
|
||||
delete(m.data, key)
|
||||
m.lock.Unlock()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Get gets a value from the DecayMap by key.
|
||||
//
|
||||
// If a value has expired, forcibly delete it if it was not updated.
|
||||
|
||||
248
docs/blog/2025-06-27-release-1.20.0/index.mdx
Normal file
248
docs/blog/2025-06-27-release-1.20.0/index.mdx
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
slug: release/v1.20.0
|
||||
title: Anubis v1.20.0 is now available!
|
||||
authors: [xe]
|
||||
tags: [release]
|
||||
image: sunburst.webp
|
||||
---
|
||||
|
||||

|
||||
|
||||
Hey all!
|
||||
|
||||
Today we released [Anubis v1.20.0: Thancred Waters](https://github.com/TecharoHQ/anubis/releases/tag/v1.20.0). This adds a lot of new and exciting features to Anubis, including but not limited to the `WEIGH` action, custom weight thresholds, Imprint/impressum support, and a no-JS challenge. Here's what you need to know so you can protect your websites in new and exciting ways!
|
||||
|
||||
{/* truncate */}
|
||||
|
||||
## Sponsoring the product
|
||||
|
||||
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).
|
||||
|
||||
I am waiting to hear back from NLNet on if Anubis was selected for funding or not. Let's hope it is!
|
||||
|
||||
## Deprecation warning: `DEFAULT_DIFFICULTY`
|
||||
|
||||
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.
|
||||
|
||||
## Chrome won't show "invalid response" after "Success!"
|
||||
|
||||
There were a bunch of smaller fixes in Anubis this time around, but the biggest one was finally squashing the ["invalid response" after "Success!" issue](https://github.com/TecharoHQ/anubis/issues/564) that had been plaguing Chrome users. This was a really annoying issue to track down but it was discovered while we were working on better end-to-end / functional testing: [Chrome randomizes the `Accept-Language` header](https://github.com/explainers-by-googlers/reduce-accept-language) so that websites can't do fingerprinting as easily.
|
||||
|
||||
When Anubis issues a challenge, it grabs [information that the browser sends to the user](/docs/design/how-anubis-works#challenge-format) to create a challenge string. Anubis doesn't store these challenge strings anywhere, and when a solution is being checked it calculates the challenge string from the request. This means that they'd get a challenge on one end, compute the response for that challenge, and then the server would validate that against a different challenge. This server-side validation would fail, leading to the user seeing "invalid response" after the client reported success.
|
||||
|
||||
I suspect this was why Vanadium and Cromite were having sporadic issues as well.
|
||||
|
||||
## New Features
|
||||
|
||||
The biggest feature in Anubis is the "weight" subsystem. This allows administrators to make custom rules that change the suspicion level of a request without having to take immediate action. As an example, consider the self-hostable git forge [Gitea](https://about.gitea.com/). When you load a page in Gitea, it creates a session cookie that your browser sends with every request. Weight allows you to mark a request that includes a Gitea session token as _less_ suspicious:
|
||||
|
||||
```yaml
|
||||
- name: gitea-session-token
|
||||
action: WEIGH
|
||||
expression:
|
||||
all:
|
||||
# Check if the request has a Cookie header
|
||||
- '"Cookie" in headers'
|
||||
# Check if the request's Cookie header contains the Gitea session token
|
||||
- headers["Cookie"].contains("i_love_gitea=")
|
||||
# Remove 5 weight points
|
||||
weight:
|
||||
adjust: -5
|
||||
```
|
||||
|
||||
This is different from the past where you could only allow every request with a Gitea session token, meaning that the invention of lying would allow malicious clients to bypass protection.
|
||||
|
||||
Weight is added and removed whenever a `WEIGH` rule is encountered. When all rules are processed and the request doesn't match any `ALLOW`, `CHALLENGE`, or `DENY` rules, Anubis uses [weight thresholds](/docs/admin/configuration/thresholds) to figure out how to handle that request. Thresholds are defined in the [policy file](/docs/admin/policies) alongside your bot rules:
|
||||
|
||||
```yaml
|
||||
thresholds:
|
||||
- name: minimal-suspicion # This client is likely fine, its soul is lighter than a feather
|
||||
expression: weight <= 0 # a feather weighs zero units
|
||||
action: ALLOW # Allow the traffic through
|
||||
# For clients that had some weight reduced through custom rules, give them a
|
||||
# lightweight challenge.
|
||||
- name: mild-suspicion
|
||||
expression:
|
||||
all:
|
||||
- weight > 0
|
||||
- weight < 10
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
|
||||
algorithm: metarefresh
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
# For clients that are browser-like but have either gained points from custom rules or
|
||||
# report as a standard browser.
|
||||
- name: moderate-suspicion
|
||||
expression:
|
||||
all:
|
||||
- weight >= 10
|
||||
- weight < 20
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||
algorithm: fast
|
||||
difficulty: 2 # two leading zeros, very fast for most clients
|
||||
report_as: 2
|
||||
# For clients that are browser like and have gained many points from custom rules
|
||||
- name: extreme-suspicion
|
||||
expression: weight >= 20
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||
algorithm: fast
|
||||
difficulty: 4
|
||||
report_as: 4
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
If you don't have thresholds defined in your Anubis policy file, Anubis will default to the "legacy" behaviour where browser-like clients get a challenge at the default difficulty.
|
||||
|
||||
:::
|
||||
|
||||
This lets most clients through if they pass a simple [proof of work challenge](/docs/admin/configuration/challenges/proof-of-work), but any clients that are less suspicious (like ones with a Gitea session token) are given the lightweight [Meta Refresh](/docs/admin/configuration/challenges/metarefresh) challenge instead.
|
||||
|
||||
Threshold expressions are like [Bot rule expressions](/docs/admin/configuration/expressions), but there's only one input: the request's weight. If no thresholds match, the request is allowed through.
|
||||
|
||||
### Imprint/Impressum Support
|
||||
|
||||
European countries like Germany [require an imprint/impressum](https://www.ionos.com/digitalguide/websites/digital-law/a-case-for-thinking-global-germanys-impressum-laws/) to be present in the footer of their website. This allows users to contact someone on the team behind a website in case they run into issues. This also must generally have a separate page where users can view an extended imprint with other information like a privacy policy or a copyright notice.
|
||||
|
||||
Anubis v1.20.0 and later [has support for showing imprints](/docs/admin/configuration/impressum). You can configure two kinds of imprints:
|
||||
|
||||
1. An imprint that is shown in the footer of every Anubis page.
|
||||
2. An extended imprint / privacy policy that is shown when users click on the "Imprint" link. For example, [here's the imprint for the website you're looking at right now](https://anubis.techaro.lol/.within.website/x/cmd/anubis/api/imprint).
|
||||
|
||||
Imprints are configured in [the policy file](/docs/admin/policies/):
|
||||
|
||||
```yaml
|
||||
impressum:
|
||||
# Displayed at the bottom of every page rendered by Anubis.
|
||||
footer: >-
|
||||
This website is hosted by Zombocom. If you have any complaints or notes
|
||||
about the service, please contact
|
||||
<a href="mailto:contact@zombocom.example">contact@zombocom.example</a> and
|
||||
we will assist you as soon as possible.
|
||||
|
||||
# The imprint page that will be linked to at the footer of every Anubis page.
|
||||
page:
|
||||
# The HTML <title> of the page
|
||||
title: Imprint and Privacy Policy
|
||||
# The HTML contents of the page. The exact contents of this page can
|
||||
# and will vary by locale. Please consult with a lawyer if you are not
|
||||
# sure what to put here.
|
||||
body: >-
|
||||
<p>Last updated: June 2025</p>
|
||||
|
||||
<h2>Information that is gathered from visitors</h2>
|
||||
|
||||
<p>In common with other websites, log files are stored on the web server
|
||||
saving details such as the visitor's IP address, browser type, referring
|
||||
page and time of visit.</p>
|
||||
|
||||
<p>Cookies may be used to remember visitor preferences when interacting
|
||||
with the website.</p>
|
||||
|
||||
<p>Where registration is required, the visitor's email and a username
|
||||
will be stored on the server.</p>
|
||||
|
||||
<!-- ... -->
|
||||
```
|
||||
|
||||
If this is insufficient, please [file an issue](https://github.com/TecharoHQ/anubis/issues/new) with a link to the relevant legislation for your country so that this feature can be amended and improved.
|
||||
|
||||
### No-JS Challenge
|
||||
|
||||
One of the first issues in Anubis before it was moved to the [TecharoHQ org](https://github.com/TecharoHQ) was a request [to support challenging browsers without using JavaScript](https://github.com/Xe/x/issues/651). This is a pretty challenging thing to do without rethinking how Anubis works from a fundamentally low level, and with v1.20.0, [Anubis finally has support for running without client-side JavaScript](https://github.com/TecharoHQ/anubis/issues/95) thanks to the [Meta Refresh](/docs/admin/configuration/challenges/metarefresh) challenge.
|
||||
|
||||
When Anubis decides it needs to send a challenge to your browser, it sends a challenge page. Historically, this challenge page is [an HTML template](https://github.com/TecharoHQ/anubis/blob/main/web/index.templ) that kicks off some JavaScript, reads the challenge information out of the page body, and then solves it as fast as possible in order to let users see the website they want to visit.
|
||||
|
||||
In v1.20.0, Anubis has a challenge registry to hold [different client challenge implementations](/docs/category/challenges). This allows us to implement anything we want as long as it can render a page to show a challenge and then check if the result is correct. This is going to be used to implement a WebAssembly-based proof of work option (one that will be way more efficient than the existing browser JS version), but as a proof of concept I implemented a simple challenge using [HTML `<meta refresh>`](https://en.wikipedia.org/wiki/Meta_refresh).
|
||||
|
||||
In my testing, this has worked with every browser I have thrown it at (including CLI browsers, the browser embedded in emacs, etc.). The default configuration of Anubis does use the [meta refresh challenge](/docs/admin/configuration/challenges/metarefresh) for [clients with a very low suspicion](/docs/admin/configuration/thresholds), but by default clients will be sent an [easy proof of work challenge](/docs/admin/configuration/challenges/proof-of-work).
|
||||
|
||||
If the false positive rate of this challenge turns out to not be very high in practice, the meta refresh challenge will be enabled by default for browsers in future versions of Anubis.
|
||||
|
||||
### `robots2policy`
|
||||
|
||||
Anubis was created because crawler bots don't respect [`robots.txt` files](https://www.robotstxt.org/). Administrators have been working on refining and crafting their `robots.txt` files for years, and one common comment is that people don't know where to start crafting their own rules.
|
||||
|
||||
Anubis now ships with a [`robots2policy` tool](/docs/admin/robots2policy) that lets you convert your `robots.txt` file to an Anubis policy.
|
||||
|
||||
```text
|
||||
robots2policy -input https://github.com/robots.txt
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
If you installed Anubis from [an OS package](/docs/admin/native-install), you may need to run `anubis-robots2policy` instead of `robots2policy`.
|
||||
|
||||
:::
|
||||
|
||||
We hope that this will help you get started with Anubis faster. We are working on a version of this that will run in the documentation via WebAssembly.
|
||||
|
||||
### Open Graph configuration is being moved to the policy file
|
||||
|
||||
Anubis supports reading [Open Graph tags](/docs/admin/configuration/open-graph) from target services and returning them in challenge pages. This makes the right metadata show up when linking services protected by Anubis in chat applications or on social media.
|
||||
|
||||
In order to test the migration of all of the configuration to the policy file, Open Graph configuration has been moved to the policy file. For more information, please read [the Open Graph configuration options](/docs/admin/configuration/open-graph#configuration-options).
|
||||
|
||||
You can also set default Open Graph tags:
|
||||
|
||||
```yaml
|
||||
openGraph:
|
||||
enabled: true
|
||||
ttl: 24h
|
||||
# If set, return these opengraph values instead of looking them up with
|
||||
# the target service.
|
||||
#
|
||||
# Correlates to properties in https://ogp.me/
|
||||
override:
|
||||
# og:title is required, it is the title of the website
|
||||
"og:title": "Techaro Anubis"
|
||||
"og:description": >-
|
||||
Anubis is a Web AI Firewall Utility that helps you fight the bots
|
||||
away so that you can maintain uptime at work!
|
||||
"description": >-
|
||||
Anubis is a Web AI Firewall Utility that helps you fight the bots
|
||||
away so that you can maintain uptime at work!
|
||||
```
|
||||
|
||||
## Improvements and optimizations
|
||||
|
||||
One of the biggest improvements we've made in v1.20.0 is replacing [SHA-256 with xxhash](https://github.com/TecharoHQ/anubis/pull/676). Anubis uses hashes all over the place to help with identifying clients, matching against rules when allowing traffic through, in error messages sent to users, and more. Historically these have been done with [SHA-256](https://en.wikipedia.org/wiki/SHA-2), however this has been having a mild performance impact in real-world use. As a result, we now use [xxhash](https://xxhash.com/) when possible. This makes policy matching 3x faster in some scenarios and reduces memory usage across the board.
|
||||
|
||||
Anubis now uses [bart](https://pkg.go.dev/github.com/gaissmai/bart) for doing IP address matching when you specify addresses in a `remote_address` check configuration or when you are matching against [advanced checks](/docs/admin/thoth). This uses the same kind of IP address routing configuration that your OS kernel does, making it very fast to query information about IP addresses. This makes IP address range matches anywhere from 3-14 times faster depending on the number of addresses it needs to match against. For more information and benchmarks, check out [@JasonLovesDoggo](https://github.com/JasonLovesDoggo)'s PR: [perf: replace cidranger with bart for significant performance improvements #675](https://github.com/TecharoHQ/anubis/pull/675).
|
||||
|
||||
## What's up next?
|
||||
|
||||
v1.21.0 is already shaping up to be a massive improvement as Anubis adds [internationalization](https://en.wikipedia.org/wiki/Internationalization) support, allowing your users to see its messages in the language they're most comfortable with.
|
||||
|
||||
So far Anubis supports the following languages:
|
||||
|
||||
- English (Simplified and Traditional)
|
||||
- French
|
||||
- Portugese (Brazil)
|
||||
- Spanish
|
||||
|
||||
If you want to contribute translations, please [file an issue](https://github.com/TecharoHQ/anubis/issues/new) with your language of choice or submit a pull request to [the `lib/localization/locales` folder](https://github.com/TecharoHQ/anubis/tree/main/lib/localization/locales). We are about to introduce features to the translation stack, so you may want to hold off a hot minute, but we welcome any and all contributions to making Anubis useful to a global audience.
|
||||
|
||||
Other things we plan to do:
|
||||
|
||||
- Move configuration to the policy file
|
||||
- Support reloading the policy file at runtime without having to restart Anubis
|
||||
- Detecting if a client is "brand new"
|
||||
- A [Valkey](https://valkey.io/)-backed store for sharing information between instances of Anubis
|
||||
- Augmenting No-JS support in the paid product
|
||||
- TLS fingerprinting
|
||||
- Automated testing improvements in CI (FreeBSD CI support, better automated integration/functional testing, etc.)
|
||||
|
||||
## Conclusion
|
||||
|
||||
I hope that these features let you get the same Anubis power you've come to know and love and increases the things you can do with it! I've been really excited to ship [thresholds](/docs/admin/configuration/thresholds) and the cloud-based services for Anubis.
|
||||
|
||||
If you run into any problems, please [file an issue](https://github.com/TecharoHQ/anubis/issues/new). Otherwise, have a good day and get back to making your communities great.
|
||||
BIN
docs/blog/2025-06-27-release-1.20.0/sunburst.webp
Normal file
BIN
docs/blog/2025-06-27-release-1.20.0/sunburst.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -1 +0,0 @@
|
||||
|
||||
@@ -11,6 +11,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
<!-- This changes the project to: -->
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
### Potentially breaking 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/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
|
||||
```
|
||||
|
||||
## v1.20.0: Thancred Waters
|
||||
|
||||
The big ticket items are as follows:
|
||||
@@ -24,6 +57,8 @@ The big ticket items are as follows:
|
||||
- Add `robots2policy` CLI utility to convert robots.txt files to Anubis challenge policies using CEL expressions ([#409](https://github.com/TecharoHQ/anubis/issues/409))
|
||||
- Refactor challenge presentation logic to use a challenge registry
|
||||
- Allow challenge implementations to register HTTP routes
|
||||
- [Imprint/Impressum support](./admin/configuration/impressum.mdx) ([#362](https://github.com/TecharoHQ/anubis/issues/362))
|
||||
- Fix "invalid response" after "Success!" in Chromium ([#564](https://github.com/TecharoHQ/anubis/issues/564))
|
||||
|
||||
A lot of performance improvements have been made:
|
||||
|
||||
@@ -34,12 +69,16 @@ A lot of performance improvements have been made:
|
||||
|
||||
And some cleanups/refactors were added:
|
||||
|
||||
- Fix OpenGraph passthrough ([#717](https://github.com/TecharoHQ/anubis/issues/717))
|
||||
- Remove the unused `/test-error` endpoint and update the testing endpoint `/make-challenge` to only be enabled in
|
||||
development
|
||||
- Add `--xff-strip-private` flag/envvar to toggle skipping X-Forwarded-For private addresses or not
|
||||
- Bump AI-robots.txt to version 1.37
|
||||
- Make progress bar styling more compatible (UXP, etc)
|
||||
- Add `--strip-base-prefix` flag/envvar to strip the base prefix from request paths when forwarding to target servers
|
||||
- Fix an off-by-one in the default threshold config
|
||||
- Add functionality for HS512 JWT algorithm
|
||||
- Add support for dynamic cookie domains with the `--cookie-dynamic-domain`/`COOKIE_DYNAMIC_DOMAIN` flag/envvar
|
||||
|
||||
Request weight is one of the biggest ticket features in Anubis. This enables Anubis to be much closer to a Web Application Firewall and when combined with custom thresholds allows administrators to have Anubis take advanced reactions. For more information about request weight, see [the request weight section](./admin/policies.mdx#request-weight) of the policy file documentation.
|
||||
|
||||
|
||||
215
docs/docs/admin/botstopper.mdx
Normal file
215
docs/docs/admin/botstopper.mdx
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
title: "Commercial support and an unbranded version"
|
||||
---
|
||||
|
||||
If you want to use Anubis but organizational policies prevent you from using the branding that the open source project ships, we offer a commercial version of Anubis named BotStopper. BotStopper builds off of the open source core of Anubis and offers organizations more control over the branding, including but not limited to:
|
||||
|
||||
- Custom images for different states of the challenge process (in process, success, failure)
|
||||
- Custom CSS and fonts
|
||||
- Custom titles for the challenge and error pages
|
||||
- "Anubis" replaced with "BotStopper" across the UI
|
||||
- A private bug tracker for issues
|
||||
|
||||
In the near future this will expand to:
|
||||
|
||||
- A private challenge implementation that does advanced fingerprinting to check if the client is a genuine browser or not
|
||||
- Advanced fingerprinting via [Thoth-based advanced checks](./thoth.mdx)
|
||||
|
||||
In order to sign up for BotStopper, please do one of the following:
|
||||
|
||||
- Sign up [on GitHub Sponsors](https://github.com/sponsors/Xe) at the $50 per month tier or higher
|
||||
- Email [sales@techaro.lol](mailto:sales@techaro.lol) with your requirements for invoicing, please note that custom invoicing will cost more than using GitHub Sponsors for understandable overhead reasons
|
||||
|
||||
## Installation
|
||||
|
||||
Install BotStopper like you would Anubis, but replace the image reference. EG:
|
||||
|
||||
```diff
|
||||
-ghcr.io/techarohq/anubis:latest
|
||||
+ghcr.io/techarohq/botstopper/anubis:latest
|
||||
```
|
||||
|
||||
### Binary packages
|
||||
|
||||
Binary packages are available [in the GitHub Releases page](https://github.com/TecharoHQ/botstopper/releases), the main difference is that the package name is `techaro-botstopper`, the systemd service is `techaro-botstopper@your-instance.service`, the binary is `/usr/bin/botstopper`, and the configuration is in `/etc/techaro-botstopper`. All other instructions in the [native package install guide](./native-install.mdx) apply.
|
||||
|
||||
### Docker / Podman
|
||||
|
||||
In order to pull the BotStopper image, you need to [authenticate with GitHub's Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry).
|
||||
|
||||
```text
|
||||
docker login ghcr.io -u your-username --password-stdin
|
||||
```
|
||||
|
||||
Then you can use the image as normal.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
If you are using Kubernetes, you will need to create an image pull secret:
|
||||
|
||||
```text
|
||||
kubectl create secret docker-registry \
|
||||
techarohq-botstopper \
|
||||
--docker-server ghcr.io \
|
||||
--docker-username your-username \
|
||||
--docker-password your-access-token \
|
||||
--docker-email your@email.address
|
||||
```
|
||||
|
||||
Then attach it to your Deployment:
|
||||
|
||||
```diff
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
+ imagePullSecrets:
|
||||
+ - name: techarohq-botstopper
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Docker compose
|
||||
|
||||
Follow [the upstream Docker compose directions](https://anubis.techaro.lol/docs/admin/environments/docker-compose) with the following additional options:
|
||||
|
||||
```diff
|
||||
anubis:
|
||||
image: ghcr.io/techarohq/botstopper/anubis:latest
|
||||
environment:
|
||||
BIND: ":8080"
|
||||
DIFFICULTY: "4"
|
||||
METRICS_BIND: ":9090"
|
||||
SERVE_ROBOTS_TXT: "true"
|
||||
TARGET: "http://nginx"
|
||||
OG_PASSTHROUGH: "true"
|
||||
OG_EXPIRY_TIME: "24h"
|
||||
|
||||
+ # botstopper config here
|
||||
+ CHALLENGE_TITLE: "Doing math for your connnection!"
|
||||
+ ERROR_TITLE: "Something went wrong!"
|
||||
+ OVERLAY_FOLDER: /assets
|
||||
+ volumes:
|
||||
+ - "./your_folder:/assets"
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
There is an example in [docker-compose.yaml](https://github.com/TecharoHQ/botstopper/blob/main/docker-compose.yaml). Start the example with `docker compose up`:
|
||||
|
||||
```text
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
And then open [https://botstopper.local.cetacean.club:8443](https://botstopper.local.cetacean.club:8443) in your browser.
|
||||
|
||||
> [!NOTE]
|
||||
> This uses locally signed sacrificial TLS certificates stored in `./demo/pki`. Your browser will rightly reject these. Here is what the example looks like:
|
||||
>
|
||||
> 
|
||||
|
||||
## Custom images and CSS
|
||||
|
||||
Anubis uses an internal filesystem that contains CSS, JavaScript, and images. The BotStopper variant of Anubis lets you specify an overlay folder with the environment variable `OVERLAY_FOLDER`. The contents of this folder will be overlaid on top of Anubis' internal filesystem, allowing you to easily customize the images and CSS.
|
||||
|
||||
Your directory tree should look like this, assuming your data is in `./your_folder`:
|
||||
|
||||
```text
|
||||
./your_folder
|
||||
└── static
|
||||
├── css
|
||||
│ └── custom.css
|
||||
└── img
|
||||
├── happy.webp
|
||||
├── pensive.webp
|
||||
└── reject.webp
|
||||
```
|
||||
|
||||
For an example directory tree using some off-the-shelf images the Tango icon set, see the [testdata](https://github.com/TecharoHQ/botstopper/tree/main/testdata/static/img) folder.
|
||||
|
||||
### Custom CSS
|
||||
|
||||
CSS customization is done mainly with CSS variables. View [the example custom CSS file](https://github.com/TecharoHQ/botstopper/blob/main/testdata/static/css/custom.css) for more information about what can be customized.
|
||||
|
||||
### Custom fonts
|
||||
|
||||
If you want to add custom fonts, copy the `woff2` files alongside your `custom.css` file and then include them with the [`@font-face` CSS at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face):
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: "Oswald";
|
||||
font-style: normal;
|
||||
font-weight: 200 900;
|
||||
font-display: swap;
|
||||
src: url("./fonts/oswald.woff2") format("woff2");
|
||||
}
|
||||
```
|
||||
|
||||
Then adjust your CSS variables accordingly:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--body-sans-font: Oswald, sans-serif;
|
||||
--body-preformatted-font: monospace;
|
||||
--body-title-font: serif;
|
||||
}
|
||||
```
|
||||
|
||||
To convert `.ttf` fonts to [Web-optimized woff2 fonts](https://www.w3.org/TR/WOFF2/), use the `woff2_compress` command from the `woff2` or `woff2-tools` package:
|
||||
|
||||
```console
|
||||
$ woff2_compress oswald.ttf
|
||||
Processing oswald.ttf => oswald.woff2
|
||||
Compressed 159517 to 70469.
|
||||
```
|
||||
|
||||
Then you can import and use it as normal.
|
||||
|
||||
### Customizing images
|
||||
|
||||
Anubis uses three images to visually communicate the state of the program. These are:
|
||||
|
||||
| Image name | Intended message | Example |
|
||||
| :------------- | :----------------------------------------------- | :-------------------------------- |
|
||||
| `happy.webp` | You have passed validation, all is good |  |
|
||||
| `pensive.webp` | Checking is running, hold steady until it's done |  |
|
||||
| `reject.webp` | Something went wrong, this is a terminal state |  |
|
||||
|
||||
To make your own images at the optimal quality, use the following ffmpeg command:
|
||||
|
||||
```text
|
||||
ffmpeg -i /path/to/image -vf scale=-1:384 happy.webp
|
||||
```
|
||||
|
||||
`ffprobe` should report something like this on the generated images:
|
||||
|
||||
```text
|
||||
Input #0, webp_pipe, from 'happy.webp':
|
||||
Duration: N/A, bitrate: N/A
|
||||
Stream #0:0: Video: webp, none, 25 fps, 25 tbr, 25 tbn
|
||||
```
|
||||
|
||||
In testing 384 by 384 pixels gives the best balance between filesize, quality, and clarity.
|
||||
|
||||
```text
|
||||
$ du -hs *
|
||||
4.0K happy.webp
|
||||
12K pensive.webp
|
||||
8.0K reject.webp
|
||||
```
|
||||
|
||||
## Customizing messages
|
||||
|
||||
You can customize messages using the following environment variables:
|
||||
|
||||
| Message | Environment variable | Default |
|
||||
| :------------------- | :------------------- | :----------------------------------------- |
|
||||
| Challenge page title | `CHALLENGE_TITLE` | `Ensuring the security of your connection` |
|
||||
| Error page title | `ERROR_TITLE` | `Error` |
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
# /etc/techaro-botstopper/gitea.env
|
||||
CHALLENGE_TITLE="Wait a moment please!"
|
||||
ERROR_TITLE="Client error"
|
||||
```
|
||||
70
docs/docs/admin/configuration/impressum.mdx
Normal file
70
docs/docs/admin/configuration/impressum.mdx
Normal file
@@ -0,0 +1,70 @@
|
||||
# Imprint / Impressum configuration
|
||||
|
||||
Some jurisdictions (such as the European Union and specifically Germany) [must have contact information freely available](https://www.privacycompany.eu/blog/the-imprint-requirement-a-must-have-for-companies-from-outside-germany) on an imprint/impressum page. Anubis supports creating an Anubis-specific imprint page for your organization with the `impressum` block in your bot policy file. For example:
|
||||
|
||||
```yaml
|
||||
impressum:
|
||||
# Displayed at the bottom of every page rendered by Anubis.
|
||||
footer: >-
|
||||
This website is hosted by Techaro. If you have any complaints or notes
|
||||
about the service, please contact
|
||||
<a href="mailto:contact@techaro.lol">contact@techaro.lol</a> and we
|
||||
will assist you as soon as possible.
|
||||
|
||||
# The imprint page that will be linked to at the footer of every Anubis page.
|
||||
page:
|
||||
# The HTML <title> of the page
|
||||
title: Imprint and Privacy Policy
|
||||
# The HTML contents of the page. The exact contents of this page can
|
||||
# and will vary by locale. Please consult with a lawyer if you are not
|
||||
# sure what to put here
|
||||
body: >-
|
||||
<p>Last updated: June 2025</p>
|
||||
|
||||
<h2>Information that is gathered from visitors</h2>
|
||||
|
||||
<p>In common with other websites, log files are stored on the web server saving details such as the visitor's IP address, browser type, referring page and time of visit.</p>
|
||||
|
||||
<p>Cookies may be used to remember visitor preferences when interacting with the website.</p>
|
||||
|
||||
<p>Where registration is required, the visitor's email and a username will be stored on the server.</p>
|
||||
|
||||
<!-- ... -->
|
||||
```
|
||||
|
||||
If you are subscribed to and using [advanced classification features](../thoth.mdx), be sure to disclose the following:
|
||||
|
||||
```html
|
||||
<h2>Techaro Anubis</h2>
|
||||
|
||||
<p>
|
||||
This website uses a service called
|
||||
<a href="https://anubis.techaro.lol">Anubis</a> by
|
||||
<a href="https://techaro.lol">Techaro</a> to filter malicious traffic. Anubis
|
||||
requires the use of browser cookies to ensure that web clients are running
|
||||
conformant software. Anubis also may report the following data to Techaro to
|
||||
improve service quality:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
IP address (for purposes of matching against geo-location and BGP autonomous
|
||||
systems numbers), which is stored in-memory and not persisted to disk.
|
||||
</li>
|
||||
<li>
|
||||
Unique browser fingerprints (such as HTTP request fingerprints and
|
||||
encryption system fingerprints), which may be stored on Techaro's side for a
|
||||
period of up to one month.
|
||||
</li>
|
||||
<li>
|
||||
HTTP request metadata that may include things such as the User-Agent header
|
||||
and other identifiers.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
This data is processed and stored for the legitimate interest of combatting
|
||||
abusive web clients. This data is encrypted at rest as much as possible and is
|
||||
only decrypted in memory for the purposes of fulfilling requests.
|
||||
</p>
|
||||
```
|
||||
@@ -34,27 +34,6 @@ These examples assume that you are using a setup where your nginx configuration
|
||||
|
||||
:::
|
||||
|
||||
## Dependencies
|
||||
|
||||
Install the following dependencies for proxying HTTP:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="rpm" label="Red Hat / RPM" default>
|
||||
|
||||
```text
|
||||
dnf -y install mod_proxy_html
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="deb" label="Debian / Ubuntu / apt">
|
||||
|
||||
```text
|
||||
apt-get install -y libapache2-mod-proxy-html libxml2-dev
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
Assuming you are protecting `anubistest.techaro.lol`, you need the following server configuration blocks:
|
||||
|
||||
@@ -64,8 +64,10 @@ Anubis uses these environment variables for configuration:
|
||||
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
||||
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
||||
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.<br/><br/>Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. |
|
||||
| `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. |
|
||||
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
||||
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||
| `COOKIE_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. 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. |
|
||||
@@ -93,11 +95,12 @@ If you don't know or understand what these settings mean, ignore them. These are
|
||||
|
||||
:::
|
||||
|
||||
| Environment Variable | Default value | Explanation |
|
||||
| :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
|
||||
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
|
||||
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
|
||||
| Environment Variable | Default value | Explanation |
|
||||
| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
|
||||
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
|
||||
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
|
||||
| `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -233,6 +233,119 @@ remote_addresses:
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Imprint / Impressum support
|
||||
|
||||
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
|
||||
|
||||
## Storage backends
|
||||
|
||||
Anubis needs to store temporary data in order to determine if a user is legitimate or not. Administrators should choose a storage backend based on their infrastructure needs. Each backend has its own advantages and disadvantages.
|
||||
|
||||
Anubis offers the following storage backends:
|
||||
|
||||
- [`memory`](#memory) -- A simple in-memory hashmap
|
||||
- [`bbolt`](#bbolt) -- An on-disk key/value store backed by [bbolt](https://github.com/etcd-io/bbolt), an embedded key/value database for Go programs
|
||||
- [`valkey`](#valkey) -- A remote in-memory key/value database backed by [Valkey](https://valkey.io/) (or another database compatible with the [RESP](https://redis.io/docs/latest/develop/reference/protocol-spec/) protocol)
|
||||
|
||||
If no storage backend is set in the policy file, Anubis will use the [`memory`](#memory) backend by default. This is equivalent to the following in the policy file:
|
||||
|
||||
```yaml
|
||||
store:
|
||||
backend: memory
|
||||
parameters: {}
|
||||
```
|
||||
|
||||
### `memory`
|
||||
|
||||
The memory backend is an in-memory cache. This backend works best if you don't use multiple instances of Anubis or don't have mutable storage in the environment you're running Anubis in.
|
||||
|
||||
| Should I use this backend? | Yes/no |
|
||||
| :------------------------------------------------------------ | :----- |
|
||||
| Are you running only one instance of Anubis for this service? | ✅ Yes |
|
||||
| Does your service get a lot of traffic? | 🚫 No |
|
||||
| Do you want to store data persistently when Anubis restarts? | 🚫 No |
|
||||
| Do you run Anubis without mutable filesystem storage? | ✅ Yes |
|
||||
|
||||
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.
|
||||
|
||||
#### Configuration
|
||||
|
||||
The memory backend does not require any configuration to use.
|
||||
|
||||
### `bbolt`
|
||||
|
||||
An on-disk storage layer powered by [bbolt](https://github.com/etcd-io/bbolt), a high performance embedded key/value database used by containerd, etcd, Kubernetes, and NATS. This backend works best if you're running Anubis on a single host and get a lot of traffic.
|
||||
|
||||
| Should I use this backend? | Yes/no |
|
||||
| :------------------------------------------------------------ | :----- |
|
||||
| Are you running only one instance of Anubis for this service? | ✅ Yes |
|
||||
| Does your service get a lot of traffic? | ✅ Yes |
|
||||
| Do you want to store data persistently when Anubis restarts? | ✅ Yes |
|
||||
| Do you run Anubis without mutable filesystem storage? | 🚫 No |
|
||||
|
||||
When Anubis opens a bbolt database, it takes an exclusive lock on that database. Other instances of Anubis or other tools cannot view the bbolt database while it is locked by another instance of Anubis. If you run multiple instances of Anubis for different services, give each its own `bbolt` configuration.
|
||||
|
||||
#### Configuration
|
||||
|
||||
The `bbolt` backend takes the following configuration options:
|
||||
|
||||
| Name | Type | Example | Description |
|
||||
| :------- | :----- | :----------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bucket` | string | `anubis` | The bbolt bucket that Anubis should place all its data into. If this is not set, then Anubis will default to the bucket `anubis`. |
|
||||
| `path` | path | `/data/anubis.bdb` | The filesystem path for the Anubis bbolt database. Anubis requires write access to the folder containing the bbolt database. |
|
||||
|
||||
Example:
|
||||
|
||||
If you have persistent storage mounted to `/data`, then your store configuration could look like this:
|
||||
|
||||
```yaml
|
||||
store:
|
||||
backend: bbolt
|
||||
parameters:
|
||||
path: /data/anubis.bdb
|
||||
```
|
||||
|
||||
### `valkey`
|
||||
|
||||
[Valkey](https://valkey.io/) is an in-memory key/value store that clients access over the network. This allows multiple instances of Anubis to share information and does not require each instance of Anubis to have persistent filesystem storage.
|
||||
|
||||
:::note
|
||||
|
||||
You can also use [Redis](http://redis.io/) with Anubis.
|
||||
|
||||
:::
|
||||
|
||||
This backend is ideal if you are running multiple instances of Anubis in a worker pool (eg: Kubernetes Deployments with a copy of Anubis in each Pod).
|
||||
|
||||
| Should I use this backend? | Yes/no |
|
||||
| :------------------------------------------------------------ | :----- |
|
||||
| Are you running only one instance of Anubis for this service? | 🚫 No |
|
||||
| Does your service get a lot of traffic? | ✅ Yes |
|
||||
| Do you want to store data persistently when Anubis restarts? | ✅ Yes |
|
||||
| Do you run Anubis without mutable filesystem storage? | ✅ Yes |
|
||||
| Do you have Redis or Valkey installed? | ✅ Yes |
|
||||
|
||||
#### Configuration
|
||||
|
||||
The `valkey` backend takes the following configuration options:
|
||||
|
||||
| Name | Type | Example | Description |
|
||||
| :---- | :----- | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `url` | string | `redis://valkey:6379/0` | The URL for the instance of Redis or Valkey that Anubis should store data in. This is in the same format as `REDIS_URL` in many cloud providers. |
|
||||
|
||||
Example:
|
||||
|
||||
If you have an instance of Valkey running with the hostname `valkey.int.techaro.lol`, then your store configuration could look like this:
|
||||
|
||||
```yaml
|
||||
store:
|
||||
backend: valkey
|
||||
parameters:
|
||||
url: "redis://valkey.int.techaro.lol:6379/0"
|
||||
```
|
||||
|
||||
This would have the Valkey client connect to host `valkey.int.techaro.lol` on port `6379` with database `0` (the default database).
|
||||
|
||||
## Risk calculation for downstream services
|
||||
|
||||
In case your service needs it for risk calculation reasons, Anubis exposes information about the rules that any requests match using a few headers:
|
||||
|
||||
@@ -107,7 +107,6 @@ This ensures that the token has enough metadata to prove that the token is valid
|
||||
Challenges are formed by taking some user request metadata and using that to generate a SHA-256 checksum. The following request headers are used:
|
||||
|
||||
- `Accept-Encoding`: The content encodings that the requestor supports, such as gzip.
|
||||
- `Accept-Language`: The language that the requestor would prefer the server respond in, such as English.
|
||||
- `X-Real-Ip`: The IP address of the requestor, as set by a reverse proxy server.
|
||||
- `User-Agent`: The user agent string of the requestor.
|
||||
- The current time in UTC rounded to the nearest week.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Why does Anubis use Proof-of-Work?
|
||||
---
|
||||
|
||||
Anubis uses a [proof of work](https://en.wikipedia.org/wiki/Proof_of_work) in order to validate that clients are genuine. The reason Anubis does this was inspired by [Hashcash](https://en.wikipedia.org/wiki/Hashcash), a suggestion from the early 2000's about extending the email protocol to avoid spam. The idea is that genuine people sending emails will have to do a small math problem that is expensive to compute, but easy to verify such as hashing a string with a given number of leading zeroes. This will have basically no impact on individuals sending a few emails a week, but the company churning out industrial quantities of advertising will be required to do prohibitively expensive computation. This is also how Bitcoin's consensus algorithm works.
|
||||
Anubis uses [proof of work](https://en.wikipedia.org/wiki/Proof_of_work) in order to validate that clients are genuine. The reason Anubis does this was inspired by [Hashcash](https://en.wikipedia.org/wiki/Hashcash), a suggestion from the early 2000's about extending the email protocol to avoid spam. The idea is that genuine people sending emails will have to do a small math problem that is expensive to compute, but easy to verify such as hashing a string with a given number of leading zeroes. This will have basically no impact on individuals sending a few emails a week, but the company churning out industrial quantities of advertising will be required to do prohibitively expensive computation. This is also how Bitcoin's consensus algorithm works.
|
||||
|
||||
## How Anubis' proof of work scheme works
|
||||
|
||||
@@ -21,16 +21,3 @@ const hash = await sha256(`${challenge}${nonce}`);
|
||||
In order to pass a challenge, the `hash` has to have the right number of leading zeros (the "difficulty"). When a client requests to pass the challenge, they include the nonce they used. The server then only has to do one sha256 operation: the one that confirms that the challenge (generated from request metadata) and the nonce (provided by the client) match the difficulty number of leading zeroes.
|
||||
|
||||
Ultimately, this is a hack whose real purpose is to give a "good enough" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to known legitimate users.
|
||||
|
||||
## Challenge format
|
||||
|
||||
Anubis generates challenges based on browser metadata, including but not limited to the following:
|
||||
|
||||
- The contents of your [`Accept-Language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language)
|
||||
- The IP address of your client
|
||||
- Your browser's [`User-Agent` string](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent)
|
||||
- The date of the current week, rooted on Sundays
|
||||
- Anubis' ed25519 public signing key for [JSON web tokens](https://jwt.io/) (JWTs)
|
||||
- The challenge difficulty
|
||||
|
||||
This is intended to be a random value that is difficult for attackers to forge and guess, but also deterministic enough that it will naturally reset itself.
|
||||
|
||||
@@ -58,7 +58,9 @@ This will build all static assets (CSS, JavaScript) for distribution.
|
||||
make build
|
||||
```
|
||||
|
||||
From this point it is up to you to make sure that `./var/anubis` ends up in the right place. You may want to consult the `./run` folder for useful files such as a systemd unit and `anubis.env.default` file.
|
||||
From this point it is up to you to make sure that `./var/anubis` and `./var/robots2policy` end up in
|
||||
the right place. You may want to consult the `./run` folder for useful files such as a systemd unit
|
||||
and `anubis.env.default` file.
|
||||
|
||||
## "Pre-baked" tarball
|
||||
|
||||
@@ -75,7 +77,7 @@ When using this tarball, all you need to do is build `./cmd/anubis`:
|
||||
make prebaked-build
|
||||
```
|
||||
|
||||
Anubis will be built to `./var/anubis`.
|
||||
Anubis will be built to `./var/anubis` and the robots2policy tool to `./var/robots2policy`.
|
||||
|
||||
## Development dependencies
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
title: Local development
|
||||
---
|
||||
|
||||
If you use an editor with [Development containers](https://containers.dev) support, load this repo's [devcontainer configuration](https://github.com/TecharoHQ/anubis/tree/main/.devcontainer). Skip to [Running Anubis locally](#running-anubis-locally) if you are using the devcontainer.
|
||||
|
||||
This enables you to contribute from [GitHub Codespaces](https://github.com/features/codespaces) or other web-based editors.
|
||||
|
||||
:::note
|
||||
|
||||
TL;DR: `npm ci && npm run dev`
|
||||
|
||||
@@ -14,6 +14,7 @@ title: Anubis
|
||||

|
||||

|
||||

|
||||
[](https://github.com/sponsors/Xe)
|
||||
|
||||
## Sponsors
|
||||
|
||||
|
||||
@@ -21,8 +21,4 @@ If you use a browser extension such as [JShelter](https://jshelter.org/), you wi
|
||||
|
||||
## Does Anubis mine Bitcoin?
|
||||
|
||||
No. Anubis does not mine Bitcoin.
|
||||
|
||||
In order to mine bitcoin, you need to download a copy of the blockchain (so you have the state required to do mining) and also broadcast your mined blocks to the network should you reach a hash with the right number of leading zeroes. You also need to continuously read for newly broadcasted transactions so you can batch them into a block. This requires gigabytes of data to be transferred from the server to the client.
|
||||
|
||||
Anubis transfers two digit numbers of kilobytes from the server to the client (which you can independently verify with your browser's Developer Tools feature). This is orders of magnitude below what is required to mine Bitcoin.
|
||||
No. Anubis does not mine Bitcoin or any other cryptocurrency.
|
||||
|
||||
@@ -45,6 +45,7 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
||||
- https://gitlab.postmarketos.org/
|
||||
- https://wiki.koha-community.org/
|
||||
- https://extensions.typo3.org/
|
||||
- https://ebird.org/
|
||||
- <details>
|
||||
<summary>FreeCAD</summary>
|
||||
- https://forum.freecad.org/
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
|
||||
|
||||
bots:
|
||||
- import: (data)/crawlers/commoncrawl.yaml
|
||||
# Pathological bots to deny
|
||||
- # This correlates to data/bots/deny-pathological.yaml in the source tree
|
||||
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/deny-pathological.yaml
|
||||
@@ -70,6 +71,55 @@ bots:
|
||||
|
||||
dnsbl: false
|
||||
|
||||
impressum:
|
||||
footer: |
|
||||
This website is hosted by Techaro. If you have any complaints or notes about the service, please contact <a href="mailto:contact@techaro.lol">contact@techaro.lol</a> and we will assist you as soon as possible.
|
||||
|
||||
page:
|
||||
title: Privacy Policy
|
||||
body: |
|
||||
<p>Last updated: June 2025</p>
|
||||
|
||||
<h2>Information that is gathered from visitors</h2>
|
||||
|
||||
<p>In common with other websites, log files are stored on the web server saving details such as the visitor's IP address, browser type, referring page and time of visit.</p>
|
||||
|
||||
<p>Cookies may be used to remember visitor preferences when interacting with the website.</p>
|
||||
|
||||
<p>Where registration is required, the visitor's email and a username will be stored on the server.</p>
|
||||
|
||||
<h2>How the Information is used</h2>
|
||||
|
||||
<p>The information is used to enhance the vistor's experience when using the website to display personalised content and possibly advertising.</p>
|
||||
|
||||
<p>E-mail addresses will not be sold, rented or leased to 3rd parties.</p>
|
||||
|
||||
<p>E-mail may be sent to inform you of news of our services or offers by us or our affiliates.</p>
|
||||
|
||||
<h2>Visitor Options</h2>
|
||||
|
||||
<p>If you have subscribed to one of our services, you may unsubscribe by following the instructions which are included in e-mail that you receive.</p>
|
||||
|
||||
<p>You may be able to block cookies via your browser settings but this may prevent you from access to certain features of the website.</p>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
|
||||
<p>Cookies are small digital signature files that are stored by your web browser that allow your preferences to be recorded when visiting the website. Also they may be used to track your return visits to the website.</p>
|
||||
|
||||
<p>3rd party advertising companies may also use cookies for tracking purposes.</p>
|
||||
|
||||
<h2>Techaro Anubis</h2>
|
||||
|
||||
<p>This website uses a service called <a href="https://anubis.techaro.lol">Anubis</a> to filter malicious traffic. Anubis requires the use of browser cookies to ensure that web clients are running conformant software. Anubis also may report the following data to Techaro to improve service quality:</p>
|
||||
|
||||
<ul>
|
||||
<li>IP address (for purposes of matching against geo-location and BGP autonomous systems numbers), which is stored in-memory and not persisted to disk.</li>
|
||||
<li>Unique browser fingerprints (such as HTTP request fingerprints and encryption system fingerprints), which may be stored on Techaro's side for a period of up to one month.</li>
|
||||
<li>HTTP request metadata that may include things such as the User-Agent header and other identifiers.</li>
|
||||
</ul>
|
||||
|
||||
<p>This data is processed and stored for the legitimate interest of combatting abusive web clients. This data is encrypted at rest as much as possible and is only decrypted in memory for the purposes of fulfilling requests.</p>
|
||||
|
||||
# By default, send HTTP 200 back to clients that either get issued a challenge
|
||||
# or a denial. This seems weird, but this is load-bearing due to the fact that
|
||||
# the most aggressive scraper bots seem to really, really, want an HTTP 200 and
|
||||
@@ -77,3 +127,8 @@ dnsbl: false
|
||||
status_codes:
|
||||
CHALLENGE: 200
|
||||
DENY: 200
|
||||
|
||||
store:
|
||||
backend: bbolt
|
||||
parameters:
|
||||
path: /xe/data/anubis/data.bdb
|
||||
|
||||
@@ -15,6 +15,8 @@ spec:
|
||||
- name: anubis
|
||||
configMap:
|
||||
name: anubis-cfg
|
||||
- name: temporary-data
|
||||
emptyDir: {}
|
||||
containers:
|
||||
- name: anubis-docs
|
||||
image: ghcr.io/techarohq/anubis/docs:main
|
||||
@@ -51,6 +53,8 @@ spec:
|
||||
volumeMounts:
|
||||
- name: anubis
|
||||
mountPath: /xe/cfg/anubis
|
||||
- name: temporary-data
|
||||
mountPath: /xe/data/anubis
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
||||
3085
docs/package-lock.json
generated
3085
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"start": "docusaurus start --host 0.0.0.0",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "echo 'use CI' && exit 1",
|
||||
@@ -15,9 +15,9 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/preset-classic": "3.7.0",
|
||||
"@docusaurus/theme-mermaid": "^3.7.0",
|
||||
"@docusaurus/core": "^3.8.1",
|
||||
"@docusaurus/preset-classic": "^3.8.1",
|
||||
"@docusaurus/theme-mermaid": "^3.8.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
@@ -25,9 +25,9 @@
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.7.0",
|
||||
"@docusaurus/tsconfig": "3.7.0",
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
"@docusaurus/tsconfig": "^3.8.1",
|
||||
"@docusaurus/types": "^3.8.1",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
BIN
docs/static/img/botstopper/example-screenshot.webp
vendored
Normal file
BIN
docs/static/img/botstopper/example-screenshot.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/static/img/botstopper/happy.webp
vendored
Normal file
BIN
docs/static/img/botstopper/happy.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
docs/static/img/botstopper/pensive.webp
vendored
Normal file
BIN
docs/static/img/botstopper/pensive.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
BIN
docs/static/img/botstopper/reject.webp
vendored
Normal file
BIN
docs/static/img/botstopper/reject.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
87
go.mod
87
go.mod
@@ -4,23 +4,28 @@ go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/TecharoHQ/thoth-proto v0.4.0
|
||||
github.com/a-h/templ v0.3.898
|
||||
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.4
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/cel-go v0.25.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.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/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
|
||||
go.etcd.io/bbolt v1.4.2
|
||||
golang.org/x/net v0.41.0
|
||||
google.golang.org/grpc v1.72.2
|
||||
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.1
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
k8s.io/apimachinery v0.33.2
|
||||
sigs.k8s.io/yaml v1.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -29,7 +34,8 @@ require (
|
||||
cel.dev/expr v0.23.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
@@ -47,69 +53,121 @@ require (
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/cli/go-gh v0.1.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/docker/docker v28.0.1+incompatible // indirect
|
||||
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.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
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.14.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-yaml v1.12.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-github/v70 v70.0.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/google/rpmpack v0.6.1-0.20250405124433-758cc6896cbc // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/goreleaser/chglog v0.7.0 // indirect
|
||||
github.com/goreleaser/fileglob v1.3.0 // indirect
|
||||
github.com/goreleaser/nfpm/v2 v2.42.1 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/natefinch/atomic v1.0.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/suzuki-shunsuke/logrus-error v0.1.4 // indirect
|
||||
github.com/suzuki-shunsuke/pinact v1.6.0 // indirect
|
||||
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 // indirect
|
||||
github.com/testcontainers/testcontainers-go v0.37.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.6 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.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.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.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.32.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.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
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
@@ -120,6 +178,9 @@ require (
|
||||
tool (
|
||||
github.com/TecharoHQ/yeet/cmd/yeet
|
||||
github.com/a-h/templ/cmd/templ
|
||||
github.com/nicksnyder/go-i18n/v2/goi18n
|
||||
github.com/suzuki-shunsuke/pinact/cmd/pinact
|
||||
golang.org/x/tools/cmd/deadcode
|
||||
golang.org/x/tools/cmd/goimports
|
||||
golang.org/x/tools/cmd/stringer
|
||||
golang.org/x/vuln/cmd/govulncheck
|
||||
|
||||
188
go.sum
188
go.sum
@@ -8,8 +8,10 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
|
||||
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
@@ -36,8 +38,8 @@ github.com/TecharoHQ/yeet v0.6.0 h1:RCBAjr7wIlllsgy0tpvWpLX7jsZgu2tiuBY3RrprcR0=
|
||||
github.com/TecharoHQ/yeet v0.6.0/go.mod h1:bj2V4Fg8qKQXoiuPZa3HuawrE8g+LsOQv/9q2WyGSsA=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
|
||||
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
||||
github.com/a-h/templ v0.3.906 h1:ZUThc8Q9n04UATaCwaG60pB1AqbulLmYEAMnWV63svg=
|
||||
github.com/a-h/templ v0.3.906/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
@@ -50,6 +52,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8=
|
||||
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
|
||||
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
|
||||
@@ -69,6 +75,14 @@ github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5
|
||||
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
@@ -79,10 +93,22 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
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.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=
|
||||
@@ -99,6 +125,8 @@ github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
@@ -117,10 +145,13 @@ github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi
|
||||
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
@@ -140,6 +171,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
|
||||
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
|
||||
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
@@ -150,9 +183,15 @@ github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
|
||||
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o=
|
||||
github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||
@@ -171,11 +210,13 @@ github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+
|
||||
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
|
||||
github.com/goreleaser/nfpm/v2 v2.42.1 h1:xu2pLRgQuz2ab+YZFoeIzwU/M5jjjCKDGwv1lRbVGvk=
|
||||
github.com/goreleaser/nfpm/v2 v2.42.1/go.mod h1:dY53KWYKebkOocxgkmpM7SRX0Nv5hU+jEu2kIaM4/LI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
@@ -187,6 +228,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d h1:RnWZeH8N8KXfbwMTex/KKMYMj0FJRCF6tQubUuQ02GM=
|
||||
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
@@ -204,12 +247,17 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
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/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=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -222,13 +270,33 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -238,6 +306,8 @@ github.com/playwright-community/playwright-go v0.5200.0/go.mod h1:UnnyQZaqUOO5yw
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
@@ -246,24 +316,34 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
|
||||
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+uQBDeAhHhwdzB5fSlVdhGcM=
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
|
||||
github.com/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.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=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
||||
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
@@ -279,29 +359,58 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/suzuki-shunsuke/logrus-error v0.1.4 h1:nWo98uba1fANHdZ9Y5pJ2RKs/PpVjrLzRp5m+mRb9KE=
|
||||
github.com/suzuki-shunsuke/logrus-error v0.1.4/go.mod h1:WsVvvw6SKSt08/fB2qbnsKIMJA4K1MYCUprqsBJbMiM=
|
||||
github.com/suzuki-shunsuke/pinact v1.6.0 h1:2QvSzREOquwLwKXhF9Hj0AInE/Rl63SZz9dKkHFC6so=
|
||||
github.com/suzuki-shunsuke/pinact v1.6.0/go.mod h1:FDUMck0mmL0mcnNZ23Vjh/aOR5cIdZhF1IIpGksT4dQ=
|
||||
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 h1:YGHgrVjGTYHY98II6zijXUHP+OyvrzSCvd8m9iUcaK8=
|
||||
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4/go.mod h1:sSi6xaUaHfaqu32ECLeyE7NTMv+ZM5dW0JikhllaalY=
|
||||
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
|
||||
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
|
||||
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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=
|
||||
@@ -312,12 +421,16 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -325,21 +438,29 @@ 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.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=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.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=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -350,6 +471,7 @@ golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
@@ -374,23 +496,27 @@ 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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
|
||||
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -409,8 +535,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
||||
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
|
||||
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
|
||||
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
|
||||
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
|
||||
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
|
||||
@@ -419,5 +545,5 @@ pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
|
||||
pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
|
||||
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
|
||||
|
||||
22
internal/unbreakdocker.go
Normal file
22
internal/unbreakdocker.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func UnbreakDocker() {
|
||||
// XXX(Xe): This is bad code. Do not do this.
|
||||
//
|
||||
// I have to do this because I'm running from inside the context of a dev
|
||||
// container. This dev container runs in a different docker network than
|
||||
// the valkey test container runs in. In order to let my dev container
|
||||
// connect to the test container, they need to share a network in common.
|
||||
// The easiest network to use for this is the default "bridge" network.
|
||||
//
|
||||
// This is a horrifying monstrosity, but the part that scares me the most
|
||||
// is the fact that it works.
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
exec.Command("docker", "network", "connect", "bridge", hostname).Run()
|
||||
}
|
||||
}
|
||||
208
lib/anubis.go
208
lib/anubis.go
@@ -1,8 +1,9 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
@@ -26,9 +28,11 @@ import (
|
||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"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"
|
||||
|
||||
// challenge implementations
|
||||
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
||||
@@ -63,35 +67,75 @@ var (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
next http.Handler
|
||||
mux *http.ServeMux
|
||||
policy *policy.ParsedConfig
|
||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||
OGTags *ogtags.OGTagCache
|
||||
cookieName string
|
||||
priv ed25519.PrivateKey
|
||||
pub ed25519.PublicKey
|
||||
opts Options
|
||||
next http.Handler
|
||||
mux *http.ServeMux
|
||||
policy *policy.ParsedConfig
|
||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||
OGTags *ogtags.OGTagCache
|
||||
ed25519Priv ed25519.PrivateKey
|
||||
hs512Secret []byte
|
||||
opts Options
|
||||
store store.Interface
|
||||
}
|
||||
|
||||
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
||||
fp := sha256.Sum256(s.pub[:])
|
||||
func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
|
||||
// return ED25519 key if HS512 is not set
|
||||
if len(s.hs512Secret) == 0 {
|
||||
return func(token *jwt.Token) (interface{}, error) {
|
||||
return s.ed25519Priv.Public().(ed25519.PublicKey), nil
|
||||
}
|
||||
} else {
|
||||
return func(token *jwt.Token) (interface{}, error) {
|
||||
return s.hs512Secret, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acceptLanguage := r.Header.Get("Accept-Language")
|
||||
if len(acceptLanguage) > 5 {
|
||||
acceptLanguage = acceptLanguage[:5]
|
||||
func (s *Server) challengeFor(r *http.Request) (*challenge.Challenge, error) {
|
||||
ckies := r.CookiesNamed(anubis.TestCookieName)
|
||||
|
||||
if len(ckies) == 0 {
|
||||
return s.issueChallenge(r.Context(), r)
|
||||
}
|
||||
|
||||
challengeData := fmt.Sprintf(
|
||||
"Accept-Language=%s,X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d",
|
||||
acceptLanguage,
|
||||
r.Header.Get("X-Real-Ip"),
|
||||
r.UserAgent(),
|
||||
time.Now().UTC().Round(24*7*time.Hour).Format(time.RFC3339),
|
||||
fp,
|
||||
difficulty,
|
||||
)
|
||||
return internal.FastHash(challengeData)
|
||||
j := store.JSON[challenge.Challenge]{Underlying: s.store}
|
||||
|
||||
ckie := ckies[0]
|
||||
chall, err := j.Get(r.Context(), "challenge:"+ckie.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chall, nil
|
||||
}
|
||||
|
||||
func (s *Server) issueChallenge(ctx context.Context, r *http.Request) (*challenge.Challenge, error) {
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var randomData = make([]byte, 256)
|
||||
if _, err := rand.Read(randomData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chall := challenge.Challenge{
|
||||
ID: id.String(),
|
||||
RandomData: fmt.Sprintf("%x", randomData),
|
||||
IssuedAt: time.Now(),
|
||||
Metadata: map[string]string{
|
||||
"User-Agent": r.Header.Get("User-Agent"),
|
||||
"X-Real-Ip": r.Header.Get("X-Real-Ip"),
|
||||
},
|
||||
}
|
||||
|
||||
j := store.JSON[challenge.Challenge]{Underlying: s.store}
|
||||
if err := j.Set(ctx, "challenge:"+id.String(), chall, 30*time.Minute); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chall, err
|
||||
}
|
||||
|
||||
func (s *Server) maybeReverseProxyHttpStatusOnly(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -114,7 +158,8 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
||||
cr, rule, err := s.check(r)
|
||||
if err != nil {
|
||||
lg.Error("check failed", "err", err)
|
||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"")
|
||||
localizer := localization.GetLocalizer(r)
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy\"", localizer.T("internal_server_error")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -133,35 +178,33 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
||||
return
|
||||
}
|
||||
|
||||
ckie, err := r.Cookie(s.cookieName)
|
||||
ckie, err := r.Cookie(anubis.CookieName)
|
||||
if err != nil {
|
||||
lg.Debug("cookie not found", "path", r.URL.Path)
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ckie.Valid(); err != nil {
|
||||
lg.Debug("cookie is invalid", "err", err)
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
|
||||
lg.Debug("cookie expired", "path", r.URL.Path)
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return s.pub, nil
|
||||
}, jwt.WithExpirationRequired(), jwt.WithStrictDecoding())
|
||||
token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, s.getTokenKeyfunc(), jwt.WithExpirationRequired(), jwt.WithStrictDecoding())
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||
return
|
||||
}
|
||||
@@ -169,7 +212,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
lg.Debug("invalid token claims type", "path", r.URL.Path)
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||
return
|
||||
}
|
||||
@@ -177,14 +220,14 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
||||
policyRule, ok := claims["policyRule"].(string)
|
||||
if !ok {
|
||||
lg.Debug("policyRule claim is not a string")
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||
return
|
||||
}
|
||||
|
||||
if policyRule != rule.Hash() {
|
||||
lg.Debug("user originally passed with a different rule, issuing new challenge", "old", policyRule, "new", rule.Name)
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.RenderIndex(w, r, rule, httpStatusOnly)
|
||||
return
|
||||
}
|
||||
@@ -200,23 +243,25 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
|
||||
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
|
||||
}
|
||||
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
switch cr.Rule {
|
||||
case config.RuleAllow:
|
||||
lg.Debug("allowing traffic to origin (explicit)")
|
||||
s.ServeHTTPNext(w, r)
|
||||
return true
|
||||
case config.RuleDeny:
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
lg.Info("explicit deny")
|
||||
if rule == nil {
|
||||
lg.Error("rule is nil, cannot calculate checksum")
|
||||
s.respondWithError(w, r, "Internal Server Error: Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.RuleDeny\"")
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.RuleDeny\"", localizer.T("internal_server_error")))
|
||||
return true
|
||||
}
|
||||
hash := rule.Hash()
|
||||
|
||||
lg.Debug("rule hash", "hash", hash)
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), s.policy.StatusCodes.Deny)
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("%s %s", localizer.T("access_denied"), hash), s.policy.StatusCodes.Deny)
|
||||
return true
|
||||
case config.RuleChallenge:
|
||||
lg.Debug("challenge requested")
|
||||
@@ -225,9 +270,9 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
|
||||
s.RenderBench(w, r)
|
||||
return true
|
||||
default:
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule)
|
||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"")
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.Rules\"", localizer.T("internal_server_error")))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -248,7 +293,12 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
||||
|
||||
if resp != dnsbl.AllGood {
|
||||
lg.Info("DNSBL hit", "status", resp.String())
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), s.policy.StatusCodes.Deny)
|
||||
localizer := localization.GetLocalizer(r)
|
||||
s.respondWithStatus(w, r, fmt.Sprintf("%s: %s, %s https://dronebl.org/lookup?ip=%s",
|
||||
localizer.T("dronebl_entry"),
|
||||
resp.String(),
|
||||
localizer.T("see_dronebl_lookup"),
|
||||
ip), s.policy.StatusCodes.Deny)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -257,6 +307,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
||||
|
||||
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
lg := internal.GetRequestLogger(r)
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
redir := r.FormValue("redir")
|
||||
if redir == "" {
|
||||
@@ -266,7 +317,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
encoder.Encode(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "Invalid invocation of MakeChallenge",
|
||||
Error: localizer.T("invalid_invocation"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -281,7 +332,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
err := encoder.Encode(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"makeChallenge\"",
|
||||
Error: fmt.Sprintf("%s \"makeChallenge\"", localizer.T("internal_server_error")),
|
||||
})
|
||||
if err != nil {
|
||||
lg.Error("failed to encode error response", "err", err)
|
||||
@@ -290,15 +341,30 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
lg = lg.With("check_result", cr)
|
||||
chal := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
|
||||
s.SetCookie(w, anubis.TestCookieName, chal, "/")
|
||||
chall, err := s.challengeFor(r)
|
||||
if err != nil {
|
||||
lg.Error("failed to fetch or issue challenge", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
err := encoder.Encode(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: fmt.Sprintf("%s \"makeChallenge\"", localizer.T("internal_server_error")),
|
||||
})
|
||||
if err != nil {
|
||||
lg.Error("failed to encode error response", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s.SetCookie(w, CookieOpts{Host: r.Host, Name: anubis.TestCookieName, Value: chall.ID})
|
||||
|
||||
err = encoder.Encode(struct {
|
||||
Rules *config.ChallengeRules `json:"rules"`
|
||||
Challenge string `json:"challenge"`
|
||||
}{
|
||||
Challenge: chal,
|
||||
Challenge: chall.RandomData,
|
||||
Rules: rule.Challenge,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -306,12 +372,13 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
lg.Debug("made challenge", "challenge", chal, "rules", rule.Challenge, "cr", cr)
|
||||
lg.Debug("made challenge", "challenge", chall, "rules", rule.Challenge, "cr", cr)
|
||||
challengesIssued.WithLabelValues("api").Inc()
|
||||
}
|
||||
|
||||
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
lg := internal.GetRequestLogger(r)
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
// Adjust cookie path if base prefix is not empty
|
||||
cookiePath := "/"
|
||||
@@ -320,20 +387,20 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if _, err := r.Cookie(anubis.TestCookieName); errors.Is(err, http.ErrNoCookie) {
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, anubis.TestCookieName, "/")
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
|
||||
lg.Warn("user has cookies disabled, this is not an anubis bug")
|
||||
s.respondWithError(w, r, "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain")
|
||||
s.respondWithError(w, r, localizer.T("cookies_disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
s.ClearCookie(w, anubis.TestCookieName, "/")
|
||||
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, "Invalid redirect")
|
||||
s.respondWithError(w, r, localizer.T("invalid_redirect"))
|
||||
return
|
||||
}
|
||||
// used by the path checker rule
|
||||
@@ -341,18 +408,18 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
urlParsed, err := r.URL.Parse(redir)
|
||||
if err != nil {
|
||||
s.respondWithError(w, r, "Redirect URL not parseable")
|
||||
s.respondWithError(w, r, localizer.T("redirect_not_parseable"))
|
||||
return
|
||||
}
|
||||
if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host {
|
||||
s.respondWithError(w, r, "Redirect domain not allowed")
|
||||
s.respondWithError(w, r, localizer.T("redirect_domain_not_allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
cr, rule, err := s.check(r)
|
||||
if err != nil {
|
||||
lg.Error("check failed", "err", err)
|
||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\"")
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s \"passChallenge\"", localizer.T("internal_server_error")))
|
||||
return
|
||||
}
|
||||
lg = lg.With("check_result", cr)
|
||||
@@ -360,16 +427,27 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||
if !ok {
|
||||
lg.Error("check failed", "err", err)
|
||||
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
|
||||
return
|
||||
}
|
||||
|
||||
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
chall, err := s.challengeFor(r)
|
||||
if err != nil {
|
||||
lg.Error("check failed", "err", err)
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
|
||||
return
|
||||
}
|
||||
|
||||
if err := impl.Validate(r, lg, rule, challengeStr); err != nil {
|
||||
in := &challenge.ValidateInput{
|
||||
Challenge: chall,
|
||||
Rule: rule,
|
||||
Store: s.store,
|
||||
}
|
||||
|
||||
if err := impl.Validate(r, lg, in); err != nil {
|
||||
failedValidations.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
||||
var cerr *challenge.Error
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
lg.Debug("challenge validate call failed", "err", err)
|
||||
|
||||
switch {
|
||||
@@ -385,19 +463,19 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// generate JWT cookie
|
||||
tokenString, err := s.signJWT(jwt.MapClaims{
|
||||
"challenge": challengeStr,
|
||||
"challenge": chall.ID,
|
||||
"method": rule.Challenge.Algorithm,
|
||||
"policyRule": rule.Hash(),
|
||||
"action": string(cr.Rule),
|
||||
})
|
||||
if err != nil {
|
||||
lg.Error("failed to sign JWT", "err", err)
|
||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||
s.respondWithError(w, r, "failed to sign JWT")
|
||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||
s.respondWithError(w, r, localizer.T("failed_to_sign_jwt"))
|
||||
return
|
||||
}
|
||||
|
||||
s.SetCookie(w, s.cookieName, tokenString, cookiePath)
|
||||
s.SetCookie(w, CookieOpts{Path: cookiePath, Host: r.Host, Value: tokenString})
|
||||
|
||||
challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
||||
lg.Debug("challenge passed, redirecting to app")
|
||||
|
||||
@@ -189,8 +189,6 @@ func TestCVE2025_24369(t *testing.T) {
|
||||
srv := spawnAnubis(t, Options{
|
||||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieName: t.Name(),
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
@@ -235,13 +233,13 @@ func TestCookieCustomExpiration(t *testing.T) {
|
||||
var ckie *http.Cookie
|
||||
for _, cookie := range resp.Cookies() {
|
||||
t.Logf("%#v", cookie)
|
||||
if cookie.Name == srv.cookieName {
|
||||
if cookie.Name == anubis.CookieName {
|
||||
ckie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
if ckie == nil {
|
||||
t.Errorf("Cookie %q not found", srv.cookieName)
|
||||
t.Errorf("Cookie %q not found", anubis.CookieName)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,7 +262,7 @@ func TestCookieSettings(t *testing.T) {
|
||||
|
||||
CookieDomain: "127.0.0.1",
|
||||
CookiePartitioned: true,
|
||||
CookieName: t.Name(),
|
||||
CookieSecure: true,
|
||||
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||
})
|
||||
|
||||
@@ -286,13 +284,13 @@ func TestCookieSettings(t *testing.T) {
|
||||
var ckie *http.Cookie
|
||||
for _, cookie := range resp.Cookies() {
|
||||
t.Logf("%#v", cookie)
|
||||
if cookie.Name == srv.cookieName {
|
||||
if cookie.Name == anubis.CookieName {
|
||||
ckie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
if ckie == nil {
|
||||
t.Errorf("Cookie %q not found", srv.cookieName)
|
||||
t.Errorf("Cookie %q not found", anubis.CookieName)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -312,6 +310,10 @@ func TestCookieSettings(t *testing.T) {
|
||||
if ckie.Partitioned != srv.opts.CookiePartitioned {
|
||||
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
|
||||
}
|
||||
|
||||
if ckie.Secure != srv.opts.CookieSecure {
|
||||
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
||||
@@ -619,7 +621,6 @@ func TestRuleChange(t *testing.T) {
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: "127.0.0.1",
|
||||
CookieName: t.Name(),
|
||||
CookieExpiration: ckieExpiration,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,52 +1,11 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
import "time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
var (
|
||||
registry map[string]Impl = map[string]Impl{}
|
||||
regLock sync.RWMutex
|
||||
)
|
||||
|
||||
func Register(name string, impl Impl) {
|
||||
regLock.Lock()
|
||||
defer regLock.Unlock()
|
||||
|
||||
registry[name] = impl
|
||||
}
|
||||
|
||||
func Get(name string) (Impl, bool) {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
result, ok := registry[name]
|
||||
return result, ok
|
||||
}
|
||||
|
||||
func Methods() []string {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
var result []string
|
||||
for method := range registry {
|
||||
result = append(result, method)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
type Impl interface {
|
||||
// Setup registers any additional routes with the Impl for assets or API routes.
|
||||
Setup(mux *http.ServeMux)
|
||||
|
||||
// Issue a new challenge to the user, called by the Anubis.
|
||||
Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error)
|
||||
|
||||
// Validate a challenge, making sure that it passes muster.
|
||||
Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error
|
||||
// Challenge is the metadata about a single challenge issuance.
|
||||
type Challenge struct {
|
||||
ID string `json:"id"` // UUID identifying the challenge
|
||||
RandomData string `json:"randomData"` // The random data the client processes
|
||||
IssuedAt time.Time `json:"issuedAt"` // When the challenge was issued
|
||||
Metadata map[string]string `json:"metadata"` // Challenge metadata such as IP address and user agent
|
||||
}
|
||||
|
||||
23
lib/challenge/challengetest/challengetest.go
Normal file
23
lib/challenge/challengetest/challengetest.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package challengetest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func New(t *testing.T) *challenge.Challenge {
|
||||
t.Helper()
|
||||
|
||||
id := uuid.Must(uuid.NewV7())
|
||||
randomData := internal.SHA256sum(time.Now().String())
|
||||
|
||||
return &challenge.Challenge{
|
||||
ID: id.String(),
|
||||
RandomData: randomData,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
7
lib/challenge/challengetest/challengetest_test.go
Normal file
7
lib/challenge/challengetest/challengetest_test.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package challengetest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
_ = New(t)
|
||||
}
|
||||
68
lib/challenge/interface.go
Normal file
68
lib/challenge/interface.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
var (
|
||||
registry map[string]Impl = map[string]Impl{}
|
||||
regLock sync.RWMutex
|
||||
)
|
||||
|
||||
func Register(name string, impl Impl) {
|
||||
regLock.Lock()
|
||||
defer regLock.Unlock()
|
||||
|
||||
registry[name] = impl
|
||||
}
|
||||
|
||||
func Get(name string) (Impl, bool) {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
result, ok := registry[name]
|
||||
return result, ok
|
||||
}
|
||||
|
||||
func Methods() []string {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
var result []string
|
||||
for method := range registry {
|
||||
result = append(result, method)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
type IssueInput struct {
|
||||
Impressum *config.Impressum
|
||||
Rule *policy.Bot
|
||||
Challenge *Challenge
|
||||
OGTags map[string]string
|
||||
Store store.Interface
|
||||
}
|
||||
|
||||
type ValidateInput struct {
|
||||
Rule *policy.Bot
|
||||
Challenge *Challenge
|
||||
Store store.Interface
|
||||
}
|
||||
|
||||
type Impl interface {
|
||||
// Setup registers any additional routes with the Impl for assets or API routes.
|
||||
Setup(mux *http.ServeMux)
|
||||
|
||||
// Issue a new challenge to the user, called by the Anubis.
|
||||
Issue(r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)
|
||||
|
||||
// Validate a challenge, making sure that it passes muster.
|
||||
Validate(r *http.Request, lg *slog.Logger, in *ValidateInput) error
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
@@ -23,7 +23,7 @@ type Impl struct{}
|
||||
|
||||
func (i *Impl) Setup(mux *http.ServeMux) {}
|
||||
|
||||
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error) {
|
||||
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) {
|
||||
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't render page: %w", err)
|
||||
@@ -31,10 +31,12 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challen
|
||||
|
||||
q := u.Query()
|
||||
q.Set("redir", r.URL.String())
|
||||
q.Set("challenge", challenge)
|
||||
q.Set("challenge", in.Challenge.RandomData)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", page(challenge, u.String(), rule.Challenge.Difficulty), challenge, rule.Challenge, ogTags)
|
||||
loc := localization.GetLocalizer(r)
|
||||
component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), page(u.String(), in.Rule.Challenge.Difficulty, loc), in.Impressum, in.Challenge.RandomData, in.Rule.Challenge, in.OGTags, loc)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't render page: %w", err)
|
||||
}
|
||||
@@ -42,11 +44,11 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challen
|
||||
return component, nil
|
||||
}
|
||||
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, wantChallenge string) error {
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
|
||||
gotChallenge := r.FormValue("challenge")
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(wantChallenge), []byte(gotChallenge)) != 1 {
|
||||
return challenge.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", challenge.ErrFailed, wantChallenge, gotChallenge))
|
||||
if subtle.ConstantTimeCompare([]byte(in.Challenge.RandomData), []byte(gotChallenge)) != 1 {
|
||||
return challenge.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", challenge.ErrFailed, in.Challenge.RandomData, gotChallenge))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
)
|
||||
|
||||
templ page(challenge, redir string, difficulty int) {
|
||||
templ page(redir string, difficulty int, loc *localization.SimpleLocalizer) {
|
||||
<div class="centered-div">
|
||||
<img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
|
||||
<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
|
||||
<p id="status">Loading...</p>
|
||||
<p>Please wait a moment while we ensure the security of your connection.</p>
|
||||
<p id="status">{ loc.T("loading") }</p>
|
||||
<p>{ loc.T("connection_security") }</p>
|
||||
<meta http-equiv="refresh" content={ fmt.Sprintf("%d; url=%s", difficulty, redir) }/>
|
||||
</div>
|
||||
}
|
||||
|
||||
43
lib/challenge/metarefresh/metarefresh_templ.go
generated
43
lib/challenge/metarefresh/metarefresh_templ.go
generated
@@ -1,6 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.898
|
||||
// templ: version: v0.3.906
|
||||
package metarefresh
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
@@ -12,9 +12,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
)
|
||||
|
||||
func page(challenge, redir string, difficulty int) templ.Component {
|
||||
func page(redir string, difficulty int, loc *localization.SimpleLocalizer) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
@@ -42,7 +43,7 @@ func page(challenge, redir string, difficulty int) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 11, Col: 165}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 165}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -55,26 +56,52 @@ func page(challenge, redir string, difficulty int) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 174}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 13, Col: 174}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p id=\"status\">Loading...</p><p>Please wait a moment while we ensure the security of your connection.</p><meta http-equiv=\"refresh\" content=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p id=\"status\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir))
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("loading"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 14, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("connection_security"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p><meta http-equiv=\"refresh\" content=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
chall "github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
@@ -28,8 +28,9 @@ func (i *Impl) Setup(mux *http.ServeMux) {
|
||||
/* no implementation required */
|
||||
}
|
||||
|
||||
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error) {
|
||||
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
||||
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
|
||||
loc := localization.GetLocalizer(r)
|
||||
component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), web.Index(loc), in.Impressum, in.Challenge.RandomData, in.Rule.Challenge, in.OGTags, loc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't render page: %w", err)
|
||||
}
|
||||
@@ -37,7 +38,10 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challen
|
||||
return component, nil
|
||||
}
|
||||
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error {
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
|
||||
rule := in.Rule
|
||||
challenge := in.Challenge.RandomData
|
||||
|
||||
nonceStr := r.FormValue("nonce")
|
||||
if nonceStr == "" {
|
||||
return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
|
||||
|
||||
@@ -124,11 +124,25 @@ func TestBasic(t *testing.T) {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
lg := slog.With()
|
||||
|
||||
if _, err := i.Issue(cs.req, lg, bot, cs.challengeStr, nil); err != nil {
|
||||
i.Setup(http.NewServeMux())
|
||||
|
||||
inp := &challenge.IssueInput{
|
||||
Rule: bot,
|
||||
Challenge: &challenge.Challenge{
|
||||
RandomData: cs.challengeStr,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := i.Issue(cs.req, lg, inp); err != nil {
|
||||
t.Errorf("can't issue challenge: %v", err)
|
||||
}
|
||||
|
||||
if err := i.Validate(cs.req, lg, bot, cs.challengeStr); !errors.Is(err, cs.err) {
|
||||
if err := i.Validate(cs.req, lg, &challenge.ValidateInput{
|
||||
Rule: bot,
|
||||
Challenge: &challenge.Challenge{
|
||||
RandomData: cs.challengeStr,
|
||||
},
|
||||
}); !errors.Is(err, cs.err) {
|
||||
t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,27 +20,31 @@ import (
|
||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/TecharoHQ/anubis/xess"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Next http.Handler
|
||||
Policy *policy.ParsedConfig
|
||||
Target string
|
||||
CookieDomain string
|
||||
CookieName string
|
||||
BasePrefix string
|
||||
WebmasterEmail string
|
||||
RedirectDomains []string
|
||||
PrivateKey ed25519.PrivateKey
|
||||
CookieExpiration time.Duration
|
||||
StripBasePrefix bool
|
||||
OpenGraph config.OpenGraph
|
||||
CookiePartitioned bool
|
||||
ServeRobotsTXT bool
|
||||
Next http.Handler
|
||||
Policy *policy.ParsedConfig
|
||||
Target string
|
||||
CookieDynamicDomain bool
|
||||
CookieDomain string
|
||||
CookieExpiration time.Duration
|
||||
CookiePartitioned bool
|
||||
BasePrefix string
|
||||
WebmasterEmail string
|
||||
RedirectDomains []string
|
||||
ED25519PrivateKey ed25519.PrivateKey
|
||||
HS512Secret []byte
|
||||
StripBasePrefix bool
|
||||
OpenGraph config.OpenGraph
|
||||
ServeRobotsTXT bool
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||
@@ -87,32 +91,26 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty
|
||||
}
|
||||
|
||||
func New(opts Options) (*Server, error) {
|
||||
if opts.PrivateKey == nil {
|
||||
if opts.ED25519PrivateKey == nil && opts.HS512Secret == nil {
|
||||
slog.Debug("opts.PrivateKey not set, generating a new one")
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lib: can't generate private key: %v", err)
|
||||
}
|
||||
opts.PrivateKey = priv
|
||||
opts.ED25519PrivateKey = priv
|
||||
}
|
||||
|
||||
anubis.BasePrefix = opts.BasePrefix
|
||||
|
||||
cookieName := anubis.CookieName
|
||||
|
||||
if opts.CookieDomain != "" {
|
||||
cookieName = anubis.WithDomainCookieName + opts.CookieDomain
|
||||
}
|
||||
|
||||
result := &Server{
|
||||
next: opts.Next,
|
||||
priv: opts.PrivateKey,
|
||||
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
||||
policy: opts.Policy,
|
||||
opts: opts,
|
||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph),
|
||||
cookieName: cookieName,
|
||||
next: opts.Next,
|
||||
ed25519Priv: opts.ED25519PrivateKey,
|
||||
hs512Secret: opts.HS512Secret,
|
||||
policy: opts.Policy,
|
||||
opts: opts,
|
||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph),
|
||||
store: opts.Policy.Store,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
@@ -149,6 +147,14 @@ func New(opts Options) (*Server, error) {
|
||||
}), "GET")
|
||||
}
|
||||
|
||||
if opts.Policy.Impressum != nil {
|
||||
registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
templ.Handler(
|
||||
web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)),
|
||||
).ServeHTTP(w, r)
|
||||
}), "GET")
|
||||
}
|
||||
|
||||
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
||||
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
||||
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
||||
|
||||
125
lib/http.go
125
lib/http.go
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -11,33 +12,81 @@ import (
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/a-h/templ"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
func (s *Server) SetCookie(w http.ResponseWriter, name, value, path string) {
|
||||
var domainMatchRegexp = regexp.MustCompile(`^((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
|
||||
|
||||
type CookieOpts struct {
|
||||
Value string
|
||||
Host string
|
||||
Path string
|
||||
Name string
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
||||
var domain = s.opts.CookieDomain
|
||||
var name = anubis.CookieName
|
||||
var path = "/"
|
||||
if cookieOpts.Name != "" {
|
||||
name = cookieOpts.Name
|
||||
}
|
||||
if cookieOpts.Path != "" {
|
||||
path = cookieOpts.Path
|
||||
}
|
||||
if s.opts.CookieDynamicDomain && domainMatchRegexp.MatchString(cookieOpts.Host) {
|
||||
if etld, err := publicsuffix.EffectiveTLDPlusOne(cookieOpts.Host); err == nil {
|
||||
domain = etld
|
||||
}
|
||||
}
|
||||
|
||||
if cookieOpts.Expiry == 0 {
|
||||
cookieOpts.Expiry = s.opts.CookieExpiration
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Expires: time.Now().Add(s.opts.CookieExpiration),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Domain: s.opts.CookieDomain,
|
||||
Value: cookieOpts.Value,
|
||||
Expires: time.Now().Add(cookieOpts.Expiry),
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
Domain: domain,
|
||||
Secure: s.opts.CookieSecure,
|
||||
Partitioned: s.opts.CookiePartitioned,
|
||||
Path: path,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) ClearCookie(w http.ResponseWriter, name, path string) {
|
||||
func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
||||
var domain = s.opts.CookieDomain
|
||||
var name = anubis.CookieName
|
||||
var path = "/"
|
||||
if cookieOpts.Name != "" {
|
||||
name = cookieOpts.Name
|
||||
}
|
||||
if cookieOpts.Path != "" {
|
||||
path = cookieOpts.Path
|
||||
}
|
||||
if s.opts.CookieDynamicDomain && domainMatchRegexp.MatchString(cookieOpts.Host) {
|
||||
if etld, err := publicsuffix.EffectiveTLDPlusOne(cookieOpts.Host); err == nil {
|
||||
domain = etld
|
||||
}
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Expires: time.Now().Add(-1 * time.Minute),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
Partitioned: s.opts.CookiePartitioned,
|
||||
Domain: s.opts.CookieDomain,
|
||||
Domain: domain,
|
||||
Secure: s.opts.CookieSecure,
|
||||
Path: path,
|
||||
})
|
||||
}
|
||||
@@ -63,9 +112,11 @@ func randomChance(n int) bool {
|
||||
}
|
||||
|
||||
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
if returnHTTPStatusOnly {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Authorization required"))
|
||||
w.Write([]byte(localizer.T("authorization_required")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,11 +124,16 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
|
||||
lg.Error("client was given a challenge but does not in fact support gzip compression")
|
||||
s.respondWithError(w, r, "Client Error: Please ensure your browser is up to date and try again later.")
|
||||
s.respondWithError(w, r, localizer.T("client_error_browser"))
|
||||
}
|
||||
|
||||
challengesIssued.WithLabelValues("embedded").Add(1)
|
||||
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
chall, err := s.challengeFor(r)
|
||||
if err != nil {
|
||||
lg.Error("can't get challenge", "err", "err")
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
|
||||
return
|
||||
}
|
||||
|
||||
var ogTags map[string]string = nil
|
||||
if s.opts.OpenGraph.Enabled {
|
||||
@@ -88,24 +144,33 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
}
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: anubis.TestCookieName,
|
||||
Value: challengeStr,
|
||||
Expires: time.Now().Add(30 * time.Minute),
|
||||
Path: "/",
|
||||
s.SetCookie(w, CookieOpts{
|
||||
Value: chall.ID,
|
||||
Host: r.Host,
|
||||
Path: "/",
|
||||
Name: anubis.TestCookieName,
|
||||
Expiry: 30 * time.Minute,
|
||||
})
|
||||
|
||||
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||
if !ok {
|
||||
lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm)
|
||||
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
|
||||
return
|
||||
}
|
||||
|
||||
component, err := impl.Issue(r, lg, rule, challengeStr, ogTags)
|
||||
in := &challenge.IssueInput{
|
||||
Impressum: s.policy.Impressum,
|
||||
Rule: rule,
|
||||
Challenge: chall,
|
||||
OGTags: ogTags,
|
||||
Store: s.store,
|
||||
}
|
||||
|
||||
component, err := impl.Issue(r, lg, in)
|
||||
if err != nil {
|
||||
lg.Error("[unexpected] render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
|
||||
s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"")
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,8 +182,10 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
}
|
||||
|
||||
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
templ.Handler(
|
||||
web.Base("Benchmarking Anubis!", web.Bench()),
|
||||
web.Base(localizer.T("benchmarking_anubis"), web.Bench(localizer), s.policy.Impressum, localizer),
|
||||
).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -127,7 +194,9 @@ func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, messag
|
||||
}
|
||||
|
||||
func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(msg, s.opts.WebmasterEmail)), templ.WithStatus(status)).ServeHTTP(w, r)
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
templ.Handler(web.Base(localizer.T("oh_noes"), web.ErrorPage(msg, s.opts.WebmasterEmail, localizer), s.policy.Impressum, localizer), templ.WithStatus(status)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -162,15 +231,17 @@ func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request {
|
||||
|
||||
func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
||||
if s.next == nil {
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
redir := r.FormValue("redir")
|
||||
urlParsed, err := r.URL.Parse(redir)
|
||||
if err != nil {
|
||||
s.respondWithStatus(w, r, "Redirect URL not parseable", http.StatusBadRequest)
|
||||
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host {
|
||||
s.respondWithStatus(w, r, "Redirect domain not allowed", http.StatusBadRequest)
|
||||
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,7 +251,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
templ.Handler(
|
||||
web.Base("You are not a bot!", web.StaticHappy()),
|
||||
web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer),
|
||||
).ServeHTTP(w, r)
|
||||
} else {
|
||||
requestsProxied.WithLabelValues(r.Host).Inc()
|
||||
@@ -194,5 +265,9 @@ func (s *Server) signJWT(claims jwt.MapClaims) (string, error) {
|
||||
claims["nbf"] = time.Now().Add(-1 * time.Minute).Unix()
|
||||
claims["exp"] = time.Now().Add(s.opts.CookieExpiration).Unix()
|
||||
|
||||
return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.priv)
|
||||
if len(s.hs512Secret) == 0 {
|
||||
return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.ed25519Priv)
|
||||
} else {
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString(s.hs512Secret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,55 @@ import (
|
||||
"github.com/TecharoHQ/anubis"
|
||||
)
|
||||
|
||||
func TestSetCookie(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
options Options
|
||||
host string
|
||||
cookieName string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
options: Options{},
|
||||
host: "",
|
||||
cookieName: anubis.CookieName,
|
||||
},
|
||||
{
|
||||
name: "domain techaro.lol",
|
||||
options: Options{CookieDomain: "techaro.lol"},
|
||||
host: "",
|
||||
cookieName: anubis.CookieName,
|
||||
},
|
||||
{
|
||||
name: "dynamic cookie domain",
|
||||
options: Options{CookieDynamicDomain: true},
|
||||
host: "techaro.lol",
|
||||
cookieName: anubis.CookieName,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
srv := spawnAnubis(t, tt.options)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
srv.SetCookie(rw, CookieOpts{Value: "test", Host: tt.host})
|
||||
|
||||
resp := rw.Result()
|
||||
cookies := resp.Cookies()
|
||||
|
||||
ckie := cookies[0]
|
||||
|
||||
if ckie.Name != tt.cookieName {
|
||||
t.Errorf("wanted cookie named %q, got cookie named %q", tt.cookieName, ckie.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearCookie(t *testing.T) {
|
||||
srv := spawnAnubis(t, Options{})
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
srv.ClearCookie(rw, srv.cookieName, "/")
|
||||
srv.ClearCookie(rw, CookieOpts{Host: "localhost"})
|
||||
|
||||
resp := rw.Result()
|
||||
|
||||
@@ -36,7 +80,7 @@ func TestClearCookieWithDomain(t *testing.T) {
|
||||
srv := spawnAnubis(t, Options{CookieDomain: "techaro.lol"})
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
srv.ClearCookie(rw, srv.cookieName, "/")
|
||||
srv.ClearCookie(rw, CookieOpts{Host: "localhost"})
|
||||
|
||||
resp := rw.Result()
|
||||
|
||||
@@ -48,8 +92,37 @@ func TestClearCookieWithDomain(t *testing.T) {
|
||||
|
||||
ckie := cookies[0]
|
||||
|
||||
if ckie.Name != srv.cookieName {
|
||||
t.Errorf("wanted cookie named %q, got cookie named %q", srv.cookieName, ckie.Name)
|
||||
if ckie.Name != anubis.CookieName {
|
||||
t.Errorf("wanted cookie named %q, got cookie named %q", anubis.CookieName, ckie.Name)
|
||||
}
|
||||
|
||||
if ckie.MaxAge != -1 {
|
||||
t.Errorf("wanted cookie max age of -1, got: %d", ckie.MaxAge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearCookieWithDynamicDomain(t *testing.T) {
|
||||
srv := spawnAnubis(t, Options{CookieDynamicDomain: true})
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
srv.ClearCookie(rw, CookieOpts{Host: "subdomain.xeiaso.net"})
|
||||
|
||||
resp := rw.Result()
|
||||
|
||||
cookies := resp.Cookies()
|
||||
|
||||
if len(cookies) != 1 {
|
||||
t.Errorf("wanted 1 cookie, got %d cookies", len(cookies))
|
||||
}
|
||||
|
||||
ckie := cookies[0]
|
||||
|
||||
if ckie.Name != anubis.CookieName {
|
||||
t.Errorf("wanted cookie named %q, got cookie named %q", anubis.CookieName, ckie.Name)
|
||||
}
|
||||
|
||||
if ckie.Domain != "xeiaso.net" {
|
||||
t.Errorf("wanted cookie domain %q, got cookie domain %q", "xeiaso.net", ckie.Domain)
|
||||
}
|
||||
|
||||
if ckie.MaxAge != -1 {
|
||||
|
||||
63
lib/localization/locales/de.json
Normal file
63
lib/localization/locales/de.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"loading": "Ladevorgang...",
|
||||
"why_am_i_seeing": "Warum sehe ich diese Seite?",
|
||||
"protected_by": "Geschützt durch",
|
||||
"made_with": "Mit ❤️ gemacht in 🇨🇦",
|
||||
"mascot_design": "Maskottchen erstellt von",
|
||||
"ai_companies_explanation": "Diese Seite wird angezeigt, da der Betreiber der Webseite Anubis eingerichtet hat, um sie vor aggressiven KI-Website-Scrapern zu schützen. Diese können Ausfälle der Webseite verursachen, wodurch die Webseite für jeden nicht erreichbar ist.",
|
||||
"anubis_compromise": "Anubis ist eine Art Kompromiss. Es verwendet die sogenannte Proof-of-Work Methode nach Hashcash, ein Mechanismus, der ursprünglich zur E-Mail-Spam-Bekämpfung entwickelt wurde. Die Idee dahinter ist, dass ein einziger User nur eine kleine Verzögerung hat, auf die Webseite zu gelangen; bei Scrapern kann das allerdings große Auswirkungen haben.",
|
||||
"hack_purpose": "Man könnte dies als eine Lösung bezeichnen, die gut genug ist, einem etwas Zeit zu verschaffen für Fingerprinting und dem Identifizieren von Headless Browsern, sodass im besten Fall normale User diese Seite garnicht erst zu sehen bekommen.",
|
||||
"jshelter_note": "Anubis benötigt moderne JavaScript-Features, welche von Plugins wie zB JShelter deaktiviert werden. Bitte deaktiviere also JShelter oder ähnliche Plugins für diese Domain.",
|
||||
"version_info": "Diese Webseite läuft mit Anubis version",
|
||||
"try_again": "Nochmal probieren",
|
||||
"go_home": "Zur Hauptseite",
|
||||
"contact_webmaster": "oder wenn es sich hier um einen Fehler handelt, kontaktiere bitte den Administrator der Webseite unter",
|
||||
"connection_security": "Bitte warte einen Moment während wir sicherstellen, dass eine sichere Verbindung verwendet wird.",
|
||||
"javascript_required": "Es muss leider JavaScript aktiviert werden, um den Check durchführen zu können. Dies ist leider notwendig weil Firmen im KI-Sektor die sozialen Verhältnisse geändert haben, wie Website-Hosting funktioniert. Eine Lösung ohne JavaScript ist in Entwicklung.",
|
||||
"benchmark_requires_js": "Das Benchmark-Tool benötigt das Aktivieren von JavaScript.",
|
||||
"difficulty": "Schwierigkeit:",
|
||||
"algorithm": "Algorithmus:",
|
||||
"compare": "Vergleich:",
|
||||
"time": "Zeit",
|
||||
"iters": "Iterationen",
|
||||
"time_a": "Zeit A",
|
||||
"iters_a": "Iterationen A",
|
||||
"time_b": "Zeit B",
|
||||
"iters_b": "Iterationen B",
|
||||
"static_check_endpoint": "Dies ist nur ein Check-Endpunkt, der von beispielsweise einem Reverse-Proxy geprüft werden kann.",
|
||||
"authorization_required": "Zugriffserlaubnis benötigt",
|
||||
"cookies_disabled": "Cookies sind in Ihrem Browser deaktiviert. Anubis benötigt Cookies um sicherzustellen, dass es sich hierbei um einen validen Zugriff handelt. Bitte aktiviere Cookies für diese Domain.",
|
||||
"access_denied": "Zugriff verweigert: Fehlercode",
|
||||
"dronebl_entry": "Eintrag in DroneBL",
|
||||
"see_dronebl_lookup": "anzeigen",
|
||||
"internal_server_error": "Interner Server Error: Misskonfiguration von Anubis. Bitte kontatkiere den Administrator damit dieser die Logs prüfen kann.",
|
||||
"invalid_redirect": "Ungültige Weiterleitung",
|
||||
"redirect_not_parseable": "URL der Weiterleitung kann nicht verarbeitet werden",
|
||||
"redirect_domain_not_allowed": "Domain der Weiterleitung nicht erlaubt",
|
||||
"failed_to_sign_jwt": "Signierung des JWT fehlgeschlagen",
|
||||
"invalid_invocation": "Aufrufen von MakeChallenge ungültig",
|
||||
"client_error_browser": "Client Error: Bitte stelle sicher, dass der Browser aktuell ist und probiere es später erneut.",
|
||||
"oh_noes": "Vermaledeit!",
|
||||
"benchmarking_anubis": "Benchmark wird durchgeführt!",
|
||||
"you_are_not_a_bot": "Sie sind kein Bot!",
|
||||
"making_sure_not_bot": "Ihr Browser wird geprüft!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Ihr Browser hat leider kein funktionierendes web.crypto Element. Wird eine sichere Verbindung verwendet?",
|
||||
"js_web_workers_error": "Ihr Browser unterstützt keine Web-Worker (Anubis verwendet diese, damit der Browser nicht unresponsive wird). Ist eventuell ein Plugin wie zB JShelter installiert?",
|
||||
"js_cookies_error": "Ihr Browser speichert keine Cookies. Anubis verwendet Cookies um ein gültiges Token zu speichern damit es wissen kann, welche Browser bereits geprüft wurden. Bitte aktiviere Cookies für diese Domain. Die Cookie-Namen von Anubis könnten sich jederzeit ändern. Cookie-Namen sind kein Teil der öffentlichen API.",
|
||||
"js_context_not_secure": "Diese Verbindung ist nicht sicher!",
|
||||
"js_context_not_secure_msg": "Bitte probiere, dich via HTTPS zu verbinden und lass den Webseiten-Administrator wissen, sauber HTTPS einzurichten. Mehr Informationen unter: <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Berechnung wird durchgeführt...",
|
||||
"js_missing_feature": "Fehlendes Feature",
|
||||
"js_challenge_error": "Fehler während des Checks!",
|
||||
"js_challenge_error_msg": "Der Check-Algorithmus konnte nicht geladen werden. Bitte lade diese Seite erneut.",
|
||||
"js_calculating_difficulty": "Berechnung wird durchgeführt...<br/>Schwierigkeit:",
|
||||
"js_speed": "Geschwindigkeit:",
|
||||
"js_verification_longer": "Der Check benötigt länger als erwartet. Bitte bleibe auf der Seite.",
|
||||
"js_success": "Erfolgreich!",
|
||||
"js_done_took": "Fertig! Dauer:",
|
||||
"js_iterations": "Iterationen",
|
||||
"js_finished_reading": "Fertig gelesen, weiter zur Seite →",
|
||||
"js_calculation_error": "Fehler bei der Berechnung!",
|
||||
"js_calculation_error_msg": "Fehler bei der Berechnung des Checks:"
|
||||
}
|
||||
63
lib/localization/locales/en.json
Normal file
63
lib/localization/locales/en.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"loading": "Loading...",
|
||||
"why_am_i_seeing": "Why am I seeing this?",
|
||||
"protected_by": "Protected by",
|
||||
"made_with": "Made with ❤️ in 🇨🇦",
|
||||
"mascot_design": "Mascot design by",
|
||||
"ai_companies_explanation": "You are seeing this because the administrator of this website has set up Anubis to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.",
|
||||
"anubis_compromise": "Anubis is a compromise. Anubis uses a Proof-of-Work scheme in the vein of Hashcash, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.",
|
||||
"hack_purpose": "Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.",
|
||||
"jshelter_note": "Please note that Anubis requires the use of modern JavaScript features that plugins like JShelter will disable. Please disable JShelter or other such plugins for this domain.",
|
||||
"version_info": "This website is running Anubis version",
|
||||
"try_again": "Try again",
|
||||
"go_home": "Go home",
|
||||
"contact_webmaster": "or if you believe you should not be blocked, please contact the webmaster at",
|
||||
"connection_security": "Please wait a moment while we ensure the security of your connection.",
|
||||
"javascript_required": "Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.",
|
||||
"benchmark_requires_js": "Running the benchmark tool requires JavaScript to be enabled.",
|
||||
"difficulty": "Difficulty:",
|
||||
"algorithm": "Algorithm:",
|
||||
"compare": "Compare:",
|
||||
"time": "Time",
|
||||
"iters": "Iters",
|
||||
"time_a": "Time A",
|
||||
"iters_a": "Iters A",
|
||||
"time_b": "Time B",
|
||||
"iters_b": "Iters B",
|
||||
"static_check_endpoint": "This is just a check endpoint for your reverse proxy to use.",
|
||||
"authorization_required": "Authorization required",
|
||||
"cookies_disabled": "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain",
|
||||
"access_denied": "Access Denied: error code",
|
||||
"dronebl_entry": "DroneBL reported an entry",
|
||||
"see_dronebl_lookup": "see",
|
||||
"internal_server_error": "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around",
|
||||
"invalid_redirect": "Invalid redirect",
|
||||
"redirect_not_parseable": "Redirect URL not parseable",
|
||||
"redirect_domain_not_allowed": "Redirect domain not allowed",
|
||||
"failed_to_sign_jwt": "failed to sign JWT",
|
||||
"invalid_invocation": "Invalid invocation of MakeChallenge",
|
||||
"client_error_browser": "Client Error: Please ensure your browser is up to date and try again later.",
|
||||
"oh_noes": "Oh noes!",
|
||||
"benchmarking_anubis": "Benchmarking Anubis!",
|
||||
"you_are_not_a_bot": "You are not a bot!",
|
||||
"making_sure_not_bot": "Making sure you're not a bot!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Your browser doesn't have a functioning web.crypto element. Are you viewing this over a secure context?",
|
||||
"js_web_workers_error": "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?",
|
||||
"js_cookies_error": "Your browser doesn't store cookies. Anubis uses cookies to determine which clients have passed challenges by storing a signed token in a cookie. Please enable storing cookies for this domain. The names of the cookies Anubis stores may vary without notice. Cookie names and values are not part of the public API.",
|
||||
"js_context_not_secure": "Your context is not secure!",
|
||||
"js_context_not_secure_msg": "Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Calculating...",
|
||||
"js_missing_feature": "Missing feature",
|
||||
"js_challenge_error": "Challenge error!",
|
||||
"js_challenge_error_msg": "Failed to resolve check algorithm. You may want to reload the page.",
|
||||
"js_calculating_difficulty": "Calculating...<br/>Difficulty:",
|
||||
"js_speed": "Speed:",
|
||||
"js_verification_longer": "Verification is taking longer than expected. Please do not refresh the page.",
|
||||
"js_success": "Success!",
|
||||
"js_done_took": "Done! Took",
|
||||
"js_iterations": "iterations",
|
||||
"js_finished_reading": "I've finished reading, continue →",
|
||||
"js_calculation_error": "Calculation error!",
|
||||
"js_calculation_error_msg": "Failed to calculate challenge:"
|
||||
}
|
||||
63
lib/localization/locales/es.json
Normal file
63
lib/localization/locales/es.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"loading": "Cargando...",
|
||||
"why_am_i_seeing": "¿Por qué veo esto?",
|
||||
"protected_by": "Protegido por",
|
||||
"made_with": "Hecho con ❤️ en 🇨🇦",
|
||||
"mascot_design": "Diseño de la mascota por",
|
||||
"ai_companies_explanation": "Ves esto porque el administrador de este sitio web ha configurado Anubis para proteger el servidor contra la plaga de empresas de IA que rastrean agresivamente los sitios web. Esto puede y causa tiempo de inactividad para los sitios web, haciendo que sus recursos sean inaccesibles para todos.",
|
||||
"anubis_compromise": "Anubis es un compromiso. Anubis utiliza un esquema de Prueba de Trabajo en la línea de Hashcash, un esquema de prueba de trabajo propuesto para reducir el spam por correo electrónico. La idea es que a escala individual, la carga adicional es insignificante, pero a escala de raspadores masivos, se acumula y hace que el raspado sea mucho más costoso.",
|
||||
"hack_purpose": "En última instancia, esto es un hack cuyo verdadero propósito es dar una solución alternativa \"suficientemente buena\" para que se pueda dedicar más tiempo a la huella digital e identificación de navegadores sin cabeza (por ejemplo: a través de cómo renderizan las fuentes) para que la página de desafío de prueba de trabajo no necesite ser presentada a usuarios que son mucho más propensos a ser legítimos.",
|
||||
"jshelter_note": "Ten en cuenta que Anubis requiere el uso de características modernas de JavaScript que plugins como JShelter deshabilitarán. Por favor, deshabilita JShelter u otros plugins similares para este dominio.",
|
||||
"version_info": "Este sitio web utiliza Anubis versión",
|
||||
"try_again": "Intentar de nuevo",
|
||||
"go_home": "Inicio",
|
||||
"contact_webmaster": "o si crees que no deberías estar bloqueado, por favor contacta al webmaster en",
|
||||
"connection_security": "Espere un momento mientras garantizamos la seguridad de su conexión.",
|
||||
"javascript_required": "Desafortunadamente, necesitas habilitar JavaScript para pasar este desafío. Esto es requerido porque las empresas de IA han cambiado el contrato social sobre cómo funciona el alojamiento de sitios web. Una solución sin JS está en desarrollo.",
|
||||
"benchmark_requires_js": "Ejecutar la herramienta de benchmark requiere que JavaScript esté habilitado.",
|
||||
"difficulty": "Dificultad:",
|
||||
"algorithm": "Algoritmo:",
|
||||
"compare": "Comparar:",
|
||||
"time": "Tiempo",
|
||||
"iters": "Iteraciones",
|
||||
"time_a": "Tiempo A",
|
||||
"iters_a": "Iter. A",
|
||||
"time_b": "Tiempo B",
|
||||
"iters_b": "Iter. B",
|
||||
"static_check_endpoint": "Este es solo un endpoint de verificación para que tu proxy inverso lo use.",
|
||||
"authorization_required": "Autorización requerida",
|
||||
"cookies_disabled": "Tu navegador está configurado para deshabilitar las cookies. Anubis requiere cookies para el interés legítimo de asegurar que eres un cliente válido. Por favor habilita las cookies para este dominio",
|
||||
"access_denied": "Acceso denegado: código de error",
|
||||
"dronebl_entry": "DroneBL reportó una entrada",
|
||||
"see_dronebl_lookup": "ver",
|
||||
"internal_server_error": "Error interno del servidor: el administrador ha configurado mal Anubis. Por favor contacta al administrador y pídele que revise los logs alrededor de",
|
||||
"invalid_redirect": "Redirección inválida",
|
||||
"redirect_not_parseable": "URL de redirección no analizable",
|
||||
"redirect_domain_not_allowed": "Dominio de redirección no permitido",
|
||||
"failed_to_sign_jwt": "falló al firmar JWT",
|
||||
"invalid_invocation": "Invocación inválida de MakeChallenge",
|
||||
"client_error_browser": "Error del cliente: Por favor asegúrate de que tu navegador esté actualizado e inténtalo de nuevo más tarde.",
|
||||
"oh_noes": "¡Oh no!",
|
||||
"benchmarking_anubis": "¡Benchmarking de Anubis!",
|
||||
"you_are_not_a_bot": "¡No eres un robot!",
|
||||
"making_sure_not_bot": "¡Asegurándonos de que no eres un robot!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Tu navegador no tiene un elemento web.crypto funcional. ¿Estás viendo esta página en un contexto seguro?",
|
||||
"js_web_workers_error": "Tu navegador no soporta web workers (Anubis los usa para evitar bloquear tu navegador). ¿Tienes un plugin como JShelter instalado?",
|
||||
"js_cookies_error": "Tu navegador no almacena cookies. Anubis usa cookies para determinar qué clientes han pasado los desafíos almacenando un token firmado en una cookie. Por favor habilita el almacenamiento de cookies para este dominio. Los nombres de las cookies que Anubis almacena pueden variar sin previo aviso. Los nombres y valores de las cookies no son parte de la API pública.",
|
||||
"js_context_not_secure": "¡Tu contexto no es seguro!",
|
||||
"js_context_not_secure_msg": "Intenta conectarte a través de HTTPS o informa al administrador para configurar HTTPS. Para más información, consulta <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Calculando...",
|
||||
"js_missing_feature": "Característica faltante",
|
||||
"js_challenge_error": "¡Error de desafío!",
|
||||
"js_challenge_error_msg": "Falló al resolver el algoritmo de verificación. Puedes intentar recargar la página.",
|
||||
"js_calculating_difficulty": "Calculando...<br/>Dificultad:",
|
||||
"js_speed": "Velocidad:",
|
||||
"js_verification_longer": "La verificación está tomando más tiempo del esperado. Por favor no actualices la página.",
|
||||
"js_success": "¡Éxito!",
|
||||
"js_done_took": "¡Terminado! Tomó",
|
||||
"js_iterations": "iteraciones",
|
||||
"js_finished_reading": "He terminado de leer, continuar →",
|
||||
"js_calculation_error": "¡Error de cálculo!",
|
||||
"js_calculation_error_msg": "Falló al calcular el desafío:"
|
||||
}
|
||||
63
lib/localization/locales/fr.json
Normal file
63
lib/localization/locales/fr.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"loading": "Chargement...",
|
||||
"why_am_i_seeing": "Pourquoi je vois ceci ?",
|
||||
"protected_by": "Protégé par",
|
||||
"made_with": "Fait avec ❤️ au 🇨🇦",
|
||||
"mascot_design": "Design de la mascotte par",
|
||||
"ai_companies_explanation": "Vous voyez ceci car l'administrateur de ce site web a configuré Anubis pour protéger le serveur contre le fléau des entreprises d'IA qui scrapent agressivement les sites web. Cela peut et cause des temps d'arrêt pour les sites web, ce qui rend leurs ressources inaccessibles pour tout le monde.",
|
||||
"anubis_compromise": "Anubis est un compromis. Anubis utilise un schéma de Preuve de Travail dans la veine de Hashcash, un schéma de preuve de travail proposé pour réduire le spam par email. L'idée est qu'à l'échelle individuelle, la charge supplémentaire est négligeable, mais à l'échelle des scrapers de masse, cela s'accumule et rend le scraping beaucoup plus coûteux.",
|
||||
"hack_purpose": "En fin de compte, c'est un hack dont le véritable objectif est de donner une solution de substitution \"assez bonne\" pour que plus de temps puisse être consacré à l'empreinte digitale et à l'identification des navigateurs sans tête (par exemple : via la façon dont ils font le rendu des polices) afin que la page de défi de preuve de travail n'ait pas besoin d'être présentée aux utilisateurs qui sont beaucoup plus susceptibles d'être légitimes.",
|
||||
"jshelter_note": "Veuillez noter qu'Anubis nécessite l'utilisation de fonctionnalités JavaScript modernes que des plugins comme JShelter désactiveront. Veuillez désactiver JShelter ou d'autres plugins similaires pour ce domaine.",
|
||||
"version_info": "Ce site web utilise Anubis version",
|
||||
"try_again": "Réessayer",
|
||||
"go_home": "Accueil",
|
||||
"contact_webmaster": "ou si vous pensez que vous ne devriez pas être bloqué, veuillez contacter le webmaster à",
|
||||
"connection_security": "Veuillez patienter un instant pendant que nous assurons la sécurité de votre connexion.",
|
||||
"javascript_required": "Malheureusement, vous devez activer JavaScript pour passer ce défi. Ceci est requis car les entreprises d'IA ont changé le contrat social autour du fonctionnement de l'hébergement de sites web. Une solution sans JS est en cours de développement.",
|
||||
"benchmark_requires_js": "L'exécution de l'outil de benchmark nécessite l'activation de JavaScript.",
|
||||
"difficulty": "Difficulté :",
|
||||
"algorithm": "Algorithme :",
|
||||
"compare": "Comparer :",
|
||||
"time": "Temps",
|
||||
"iters": "Itérations",
|
||||
"time_a": "Temps A",
|
||||
"iters_a": "Itér. A",
|
||||
"time_b": "Temps B",
|
||||
"iters_b": "Itér. B",
|
||||
"static_check_endpoint": "Ceci est juste un point de terminaison de vérification pour votre proxy inverse à utiliser.",
|
||||
"authorization_required": "Autorisation requise",
|
||||
"cookies_disabled": "Votre navigateur est configuré pour désactiver les cookies. Anubis nécessite des cookies pour l'intérêt légitime de s'assurer que vous êtes un client valide. Veuillez activer les cookies pour ce domaine",
|
||||
"access_denied": "Accès refusé : code d'erreur",
|
||||
"dronebl_entry": "DroneBL a signalé une entrée",
|
||||
"see_dronebl_lookup": "voir",
|
||||
"internal_server_error": "Erreur interne du serveur : l'administrateur a mal configuré Anubis. Veuillez contacter l'administrateur et lui demander de consulter les logs autour de",
|
||||
"invalid_redirect": "Redirection invalide",
|
||||
"redirect_not_parseable": "URL de redirection non analysable",
|
||||
"redirect_domain_not_allowed": "Domaine de redirection non autorisé",
|
||||
"failed_to_sign_jwt": "échec de la signature JWT",
|
||||
"invalid_invocation": "Invocation invalide de MakeChallenge",
|
||||
"client_error_browser": "Erreur client : Veuillez vous assurer que votre navigateur est à jour et réessayez plus tard.",
|
||||
"oh_noes": "Oh non !",
|
||||
"benchmarking_anubis": "Test de performance d'Anubis !",
|
||||
"you_are_not_a_bot": "Vous n'êtes pas un robot !",
|
||||
"making_sure_not_bot": "Vérification que vous n'êtes pas un robot !",
|
||||
"celphase": "PHASE de CEL",
|
||||
"js_web_crypto_error": "Votre navigateur n'a pas d'élément web.crypto fonctionnel. Consultez-vous cette page dans un contexte sécurisé ?",
|
||||
"js_web_workers_error": "Votre navigateur ne prend pas en charge les web workers (Anubis les utilise pour éviter de bloquer votre navigateur). Avez-vous un plugin comme JShelter installé ?",
|
||||
"js_cookies_error": "Votre navigateur ne stocke pas les cookies. Anubis utilise des cookies pour déterminer quels clients ont réussi les défis en stockant un jeton signé dans un cookie. Veuillez activer le stockage des cookies pour ce domaine. Les noms des cookies qu'Anubis stocke peuvent varier sans préavis. Les noms et valeurs des cookies ne font pas partie de l'API publique.",
|
||||
"js_context_not_secure": "Votre contexte n'est pas sécurisé !",
|
||||
"js_context_not_secure_msg": "Essayez de vous connecter via HTTPS ou informez l'administrateur de configurer HTTPS. Pour plus d'informations, voir <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Calcul en cours...",
|
||||
"js_missing_feature": "Fonctionnalité manquante",
|
||||
"js_challenge_error": "Erreur de défi !",
|
||||
"js_challenge_error_msg": "Échec de la résolution de l'algorithme de vérification. Vous pouvez essayer de recharger la page.",
|
||||
"js_calculating_difficulty": "Calcul en cours...<br/>Difficulté :",
|
||||
"js_speed": "Vitesse :",
|
||||
"js_verification_longer": "La vérification prend plus de temps que prévu. Veuillez ne pas actualiser la page.",
|
||||
"js_success": "Succès !",
|
||||
"js_done_took": "Terminé ! A pris",
|
||||
"js_iterations": "itérations",
|
||||
"js_finished_reading": "J'ai fini de lire, continuer →",
|
||||
"js_calculation_error": "Erreur de calcul !",
|
||||
"js_calculation_error_msg": "Échec du calcul du défi :"
|
||||
}
|
||||
3
lib/localization/locales/manifest.json
Normal file
3
lib/localization/locales/manifest.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"supportedLanguages": ["en", "fr", "es", "pt-BR", "de", "tr"]
|
||||
}
|
||||
63
lib/localization/locales/pt-BR.json
Normal file
63
lib/localization/locales/pt-BR.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"loading": "Carregando...",
|
||||
"why_am_i_seeing": "Por que estou vendo isso?",
|
||||
"protected_by": "Protegido por",
|
||||
"made_with": "Feito com ❤️ no 🇨🇦",
|
||||
"mascot_design": "Design do mascote por",
|
||||
"ai_companies_explanation": "Você está vendo isso porque o administrador deste site configurou Anubis para proteger o servidor contra a praga de empresas de IA que realizam scraping agressivo em sites. Isso pode causar, e de fato causa, inoperância nos sites, o que torna seus recursos inacessíveis para todos.",
|
||||
"anubis_compromise": "O Anubis é um meio-termo. Ele utiliza um esquema de Prova de Trabalho (Proof-of-Work) semelhante ao Hashcash, um esquema de Prova de Trabalho proposto para reduzir spam de e-mail. A ideia é que, em escalas individuais, a carga adicional seja insignificante, mas em níveis em massa de scrapers, ela se acumula e torna o scraping muito mais custoso.",
|
||||
"hack_purpose": "Em última análise, este é um hack cujo propósito real é fornecer uma solução \"boa o suficiente\" para que mais tempo possa ser gasto na identificação de navegadores sem interface (por exemplo: por meio de como eles fazem a renderização de fontes), para que a página do desafio da prova de trabalho não precise ser apresentada a usuários que têm muito mais probabilidade de serem legítimos.",
|
||||
"jshelter_note": "Observe que o Anubis requer o uso de recursos JavaScript modernos que plugins como o JShelter desabilitarão. Desabilite o JShelter ou outros plugins semelhantes para este domínio.",
|
||||
"version_info": "Este site está usando o Anubis versão",
|
||||
"try_again": "Tente novamente",
|
||||
"go_home": "Início",
|
||||
"contact_webmaster": "ou se você acredita que não deveria estar bloqueado, contate o webmaster em",
|
||||
"connection_security": "Por favor, aguarde um momento enquanto nós garantimos a segurança de sua conexão.",
|
||||
"javascript_required": "Infelizmente, você deve habilitar JavaScript para passar por este desafio. Isso é necessário porque empresas de IA alteraram o contrato social sobre como a hospedagem de sites funciona. Uma solução que não use JavaScript ainda está sendo desenvolvida.",
|
||||
"benchmark_requires_js": "Para executar a ferramenta de benchmark, é necessário que o JavaScript esteja habilitado.",
|
||||
"difficulty": "Dificuldade:",
|
||||
"algorithm": "Algoritmo:",
|
||||
"compare": "Comparar:",
|
||||
"time": "Tempo",
|
||||
"iters": "Iteração",
|
||||
"time_a": "Tempo A",
|
||||
"iters_a": "Iteração A",
|
||||
"time_b": "Tempo B",
|
||||
"iters_b": "Iteração B",
|
||||
"static_check_endpoint": "Este é apenas um ponto de verificação para seu proxy reverso usar.",
|
||||
"authorization_required": "Autorização necessária",
|
||||
"cookies_disabled": "Seu navegador está configurado para desabilitar cookies. O Anubis requer cookies para o interesse legítimo de garantir que você seja um cliente válido. Habilite os cookies para este domínio.",
|
||||
"access_denied": "Acesso negado: código de erro",
|
||||
"dronebl_entry": "DroneBL relatou uma entrada",
|
||||
"see_dronebl_lookup": "consulte",
|
||||
"internal_server_error": "Erro interno do servidor: o administrador configurou incorretamente o Anubis. Entre em contato com o administrador e peça para analisar os logs relacionados.",
|
||||
"invalid_redirect": "Redirecionamento inválido",
|
||||
"redirect_not_parseable": "URL de redirecionamento não analisável",
|
||||
"redirect_domain_not_allowed": "Domínio de redirecionamento não permitido",
|
||||
"failed_to_sign_jwt": "falha ao assinar JWT",
|
||||
"invalid_invocation": "Invocação inválida de MakeChallenge",
|
||||
"client_error_browser": "Erro do cliente: verifique se seu navegador está atualizado e tente novamente mais tarde..",
|
||||
"oh_noes": "Ah, não!",
|
||||
"benchmarking_anubis": "Fazendo benchmark do Anubis!",
|
||||
"you_are_not_a_bot": "Você não é um bot!",
|
||||
"making_sure_not_bot": "Certificando de que você não é um bot!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Seu navegador não possui um elemento web.crypto funcional. Você está visualizando isso em um contexto seguro?",
|
||||
"js_web_workers_error": "Seu navegador não oferece suporte a web workers (o Anubis usa isso para evitar que seu navegador trave). Você tem um plugin como o JShelter instalado?",
|
||||
"js_cookies_error": "Seu navegador não armazena cookies. O Anubis usa cookies para determinar quais clientes passaram nos desafios, armazenando um token assinado em um cookie. Habilite o armazenamento de cookies para este domínio. Os nomes dos cookies armazenados pelo Anubis podem variar sem aviso prévio. Os nomes e valores dos cookies não fazem parte da API pública.",
|
||||
"js_context_not_secure": "Seu contexto não é seguro!",
|
||||
"js_context_not_secure_msg": "Tente conectar-se via HTTPS ou avise o administrador para configurar o HTTPS. Para mais informações, consulte o <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Calculando...",
|
||||
"js_missing_feature": "Faltando recurso",
|
||||
"js_challenge_error": "Erro no desafio!",
|
||||
"js_challenge_error_msg": "Falha ao resolver o algoritmo de verificação. Talvez seja necessário recarregar a página.",
|
||||
"js_calculating_difficulty": "Calculando...<br/>Dificuldade:",
|
||||
"js_speed": "Velocidade:",
|
||||
"js_verification_longer": "A verificação está demorando mais do que o esperado. Não atualize a página.",
|
||||
"js_success": "Sucesso!",
|
||||
"js_done_took": "Feito! Levou",
|
||||
"js_iterations": "iterações",
|
||||
"js_finished_reading": "Terminei de ler, continue →",
|
||||
"js_calculation_error": "Erro de cálculo!",
|
||||
"js_calculation_error_msg": "Falha ao calcular o desafio:"
|
||||
}
|
||||
63
lib/localization/locales/tr.json
Normal file
63
lib/localization/locales/tr.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"loading": "Yükleniyor...",
|
||||
"why_am_i_seeing": "Bunu neden görüyorum?",
|
||||
"protected_by": "Koruma sağlayan:",
|
||||
"made_with": "🇨🇦'da ❤️ ile yapıldı",
|
||||
"mascot_design": "Maskot tasarımı:",
|
||||
"ai_companies_explanation": "Bunu görüyorsunuz çünkü bu web sitesinin yöneticisi, yapay zeka şirketlerinin web sitelerini agresif şekilde kazımasına karşı sunucuyu korumak için Anubis'i kurdu. Bu tarz kazımalar sitelerin erişilemez olmasına ve kesintilere neden olabiliyor.",
|
||||
"anubis_compromise": "Anubis bir uzlaşmadır. Anubis, spam e-postaları azaltmak için önerilen bir iş kanıtı sistemi olan Hashcash benzeri bir sistemi kullanır. Bireysel kullanımda bu ek yük göz ardı edilebilir olsa da, büyük ölçekli kazıyıcılarda birikerek kazımayı oldukça maliyetli hale getirir.",
|
||||
"hack_purpose": "Bu geçici bir çözümdür. Esas amacı, başsız tarayıcıları parmak iziyle tanımlamak için daha fazla zaman kazandırmak, ve bu sayede meşru kullanıcıların bu zorluk sayfasını görmesini önlemektir.",
|
||||
"jshelter_note": "Lütfen dikkat: Anubis, JShelter gibi eklentilerin devre dışı bıraktığı modern JavaScript özelliklerini gerektirir. Lütfen bu alan adı için JShelter veya benzeri eklentileri devre dışı bırakın.",
|
||||
"version_info": "Bu web sitesi şu Anubis sürümünü çalıştırıyor:",
|
||||
"try_again": "Tekrar dene",
|
||||
"go_home": "Ana sayfaya dön",
|
||||
"contact_webmaster": "ya da engellenmemeniz gerektiğini düşünüyorsanız, lütfen şu adrese e-posta gönderin:",
|
||||
"connection_security": "Bağlantınızın güvenliği sağlanırken lütfen bekleyin.",
|
||||
"javascript_required": "Ne yazık ki bu aşamayı geçebilmek için JavaScript’i etkinleştirmeniz gerekiyor. Bunun nedeni, yapay zeka şirketlerinin web barındırma konusundaki sosyal sözleşmeyi değiştirmiş olmasıdır. JavaScript’siz bir çözüm geliştirilmektedir.",
|
||||
"benchmark_requires_js": "Kıyaslama aracının çalıştırılması için JavaScript’in etkin olması gereklidir.",
|
||||
"difficulty": "Zorluk:",
|
||||
"algorithm": "Algoritma:",
|
||||
"compare": "Karşılaştır:",
|
||||
"time": "Süre",
|
||||
"iters": "Tekrar",
|
||||
"time_a": "Süre A",
|
||||
"iters_a": "Tekrar A",
|
||||
"time_b": "Süre B",
|
||||
"iters_b": "Tekrar B",
|
||||
"static_check_endpoint": "Bu sadece ters proxy'nizin kullanması için bir kontrol adresidir.",
|
||||
"authorization_required": "Yetkilendirme gerekli",
|
||||
"cookies_disabled": "Tarayıcınız çerezleri devre dışı bırakacak şekilde yapılandırılmış. Anubis, gerçek bir kullanıcı olduğunuzu doğrulamak için çerezlere ihtiyaç duyar. Lütfen bu alan adı için çerezleri etkinleştirin.",
|
||||
"access_denied": "Erişim Reddedildi: hata kodu",
|
||||
"dronebl_entry": "DroneBL bir giriş bildirdi",
|
||||
"see_dronebl_lookup": "bakınız",
|
||||
"internal_server_error": "Sunucu Hatası: Yönetici Anubis'i yanlış yapılandırmış. Lütfen yöneticinizle iletişime geçin ve şunun civarındaki kayıtlara bakmasını isteyin:",
|
||||
"invalid_redirect": "Geçersiz yönlendirme",
|
||||
"redirect_not_parseable": "Yönlendirme URL'si çözümlenemiyor",
|
||||
"redirect_domain_not_allowed": "Yönlendirme alan adına izin verilmiyor",
|
||||
"failed_to_sign_jwt": "JWT imzalanamadı",
|
||||
"invalid_invocation": "Geçersiz MakeChallenge çağrısı",
|
||||
"client_error_browser": "İstemci Hatası: Lütfen tarayıcınızın güncel olduğundan emin olun ve daha sonra tekrar deneyin.",
|
||||
"oh_noes": "Ah hayır!",
|
||||
"benchmarking_anubis": "Anubis kıyaslanıyor!",
|
||||
"you_are_not_a_bot": "Bot değilsiniz!",
|
||||
"making_sure_not_bot": "Bot olmadığınızdan emin oluyoruz!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Tarayıcınızda çalışan bir web.crypto öğesi yok. Bu sayfayı güvenli bir bağlantı üzerinden mi görüntülüyorsunuz?",
|
||||
"js_web_workers_error": "Tarayıcınız web işçilerini desteklemiyor (Anubis, tarayıcınızın donmaması için bunları kullanır). JShelter gibi bir eklenti mi kurulu?",
|
||||
"js_cookies_error": "Tarayıcınız çerezleri kaydetmiyor. Anubis, kullanıcıların zorlukları geçtiğini belirlemek için imzalı bir belirteci çerezde saklar. Lütfen bu alan adı için çerezleri etkinleştirin. Anubis’in kullandığı çerez adları önceden bildirilmeksizin değişebilir. Çerez adları ve değerleri resmi API’nin bir parçası değildir.",
|
||||
"js_context_not_secure": "Bağlantınız güvenli değil!",
|
||||
"js_context_not_secure_msg": "HTTPS üzerinden bağlanmayı deneyin veya yöneticiden HTTPS kurulumu yapmasını isteyin. Daha fazla bilgi için bkz. <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Hesaplanıyor...",
|
||||
"js_missing_feature": "Eksik özellik",
|
||||
"js_challenge_error": "Hesaplama hatası!",
|
||||
"js_challenge_error_msg": "Algoritma çözümlemesi başarısız oldu. Sayfayı yeniden yüklemeyi deneyebilirsiniz.",
|
||||
"js_calculating_difficulty": "Hesaplanıyor...<br/>Zorluk:",
|
||||
"js_speed": "Hız:",
|
||||
"js_verification_longer": "Doğrulama beklenenden uzun sürüyor. Lütfen sayfayı yenilemeyin.",
|
||||
"js_success": "Başarılı!",
|
||||
"js_done_took": "Tamamlandı! Süre:",
|
||||
"js_iterations": "tekrar",
|
||||
"js_finished_reading": "Okumayı bitirdim, devam et →",
|
||||
"js_calculation_error": "Hesaplama hatası!",
|
||||
"js_calculation_error_msg": "Zorluk hesaplaması başarısız oldu:"
|
||||
}
|
||||
106
lib/localization/localization.go
Normal file
106
lib/localization/localization.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package localization
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
//go:embed locales/*.json
|
||||
var localeFS embed.FS
|
||||
|
||||
type LocalizationService struct {
|
||||
bundle *i18n.Bundle
|
||||
}
|
||||
|
||||
var (
|
||||
globalService *LocalizationService
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func NewLocalizationService() *LocalizationService {
|
||||
once.Do(func() {
|
||||
bundle := i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||
|
||||
// Read all JSON files from the locales directory
|
||||
entries, err := localeFS.ReadDir("locales")
|
||||
if err != nil {
|
||||
// Try fallback - create a minimal service with default messages
|
||||
globalService = &LocalizationService{bundle: bundle}
|
||||
return
|
||||
}
|
||||
|
||||
loadedAny := false
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") {
|
||||
filePath := "locales/" + entry.Name()
|
||||
_, err := bundle.LoadMessageFileFS(localeFS, filePath)
|
||||
if err != nil {
|
||||
// Log error but continue with other files
|
||||
continue
|
||||
}
|
||||
loadedAny = true
|
||||
}
|
||||
}
|
||||
|
||||
if !loadedAny {
|
||||
// If no files were loaded successfully, create minimal service
|
||||
globalService = &LocalizationService{bundle: bundle}
|
||||
return
|
||||
}
|
||||
|
||||
globalService = &LocalizationService{bundle: bundle}
|
||||
})
|
||||
|
||||
// Safety check - if globalService is still nil, create a minimal one
|
||||
if globalService == nil {
|
||||
bundle := i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||
globalService = &LocalizationService{bundle: bundle}
|
||||
}
|
||||
|
||||
return globalService
|
||||
}
|
||||
|
||||
func (ls *LocalizationService) GetLocalizer(lang string) *i18n.Localizer {
|
||||
return i18n.NewLocalizer(ls.bundle, lang)
|
||||
}
|
||||
|
||||
func (ls *LocalizationService) GetLocalizerFromRequest(r *http.Request) *i18n.Localizer {
|
||||
if ls == nil || ls.bundle == nil {
|
||||
// Fallback to a basic bundle if service is not properly initialized
|
||||
bundle := i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||
return i18n.NewLocalizer(bundle, "en")
|
||||
}
|
||||
acceptLanguage := r.Header.Get("Accept-Language")
|
||||
return i18n.NewLocalizer(ls.bundle, acceptLanguage, "en")
|
||||
}
|
||||
|
||||
// SimpleLocalizer wraps i18n.Localizer with a more convenient API
|
||||
type SimpleLocalizer struct {
|
||||
Localizer *i18n.Localizer
|
||||
}
|
||||
|
||||
// T provides a concise way to localize messages
|
||||
func (sl *SimpleLocalizer) T(messageID string) string {
|
||||
return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
|
||||
}
|
||||
|
||||
// GetLocalizer creates a localizer based on the request's Accept-Language header or forcedLanguage option
|
||||
func GetLocalizer(r *http.Request) *SimpleLocalizer {
|
||||
var localizer *i18n.Localizer
|
||||
if anubis.ForcedLanguage == "" {
|
||||
localizer = NewLocalizationService().GetLocalizerFromRequest(r)
|
||||
} else {
|
||||
localizer = NewLocalizationService().GetLocalizer(anubis.ForcedLanguage)
|
||||
}
|
||||
return &SimpleLocalizer{Localizer: localizer}
|
||||
}
|
||||
147
lib/localization/localization_test.go
Normal file
147
lib/localization/localization_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package localization
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
)
|
||||
|
||||
func TestLocalizationService(t *testing.T) {
|
||||
service := NewLocalizationService()
|
||||
|
||||
t.Run("English localization", func(t *testing.T) {
|
||||
localizer := service.GetLocalizer("en")
|
||||
result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"})
|
||||
if result != "Loading..." {
|
||||
t.Errorf("Expected 'Loading...', got '%s'", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("French localization", func(t *testing.T) {
|
||||
localizer := service.GetLocalizer("fr")
|
||||
result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"})
|
||||
if result != "Chargement..." {
|
||||
t.Errorf("Expected 'Chargement...', got '%s'", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("German localization", func(t *testing.T) {
|
||||
localizer := service.GetLocalizer("de")
|
||||
result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"})
|
||||
if result != "Ladevorgang..." {
|
||||
t.Errorf("Expected 'Ladevorgang...', got '%s'", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Turkish localization", func(t *testing.T) {
|
||||
localizer := service.GetLocalizer("tr")
|
||||
result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"})
|
||||
if result != "Yükleniyor..." {
|
||||
t.Errorf("Expected 'Yükleniyor...', got '%s'", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All required keys exist in English", func(t *testing.T) {
|
||||
localizer := service.GetLocalizer("en")
|
||||
requiredKeys := []string{
|
||||
"loading", "why_am_i_seeing", "protected_by", "made_with",
|
||||
"mascot_design", "try_again", "go_home", "javascript_required",
|
||||
}
|
||||
|
||||
for _, key := range requiredKeys {
|
||||
result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: key})
|
||||
if result == "" {
|
||||
t.Errorf("Key '%s' returned empty string", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All required keys exist in French", func(t *testing.T) {
|
||||
localizer := service.GetLocalizer("fr")
|
||||
requiredKeys := []string{
|
||||
"loading", "why_am_i_seeing", "protected_by", "made_with",
|
||||
"mascot_design", "try_again", "go_home", "javascript_required",
|
||||
}
|
||||
|
||||
for _, key := range requiredKeys {
|
||||
result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: key})
|
||||
if result == "" {
|
||||
t.Errorf("Key '%s' returned empty string", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All required keys exist in Turkish", func(t *testing.T) {
|
||||
localizer := service.GetLocalizer("tr")
|
||||
requiredKeys := []string{
|
||||
"loading", "why_am_i_seeing", "protected_by", "made_with",
|
||||
"mascot_design", "try_again", "go_home", "javascript_required",
|
||||
}
|
||||
|
||||
for _, key := range requiredKeys {
|
||||
result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: key})
|
||||
if result == "" {
|
||||
t.Errorf("Key '%s' returned empty string", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
SupportedLanguages []string `json:"supported_languages"`
|
||||
}
|
||||
|
||||
func loadManifest(t *testing.T) manifest {
|
||||
t.Helper()
|
||||
|
||||
fin, err := localeFS.Open("locales/manifest.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
var result manifest
|
||||
if err := json.NewDecoder(fin).Decode(&result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func TestComprehensiveTranslations(t *testing.T) {
|
||||
service := NewLocalizationService()
|
||||
|
||||
var translations = map[string]any{}
|
||||
fin, err := localeFS.Open("locales/en.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
if err := json.NewDecoder(fin).Decode(&translations); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range translations {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, lang := range loadManifest(t).SupportedLanguages {
|
||||
t.Run(lang, func(t *testing.T) {
|
||||
loc := service.GetLocalizer(lang)
|
||||
sl := SimpleLocalizer{Localizer: loc}
|
||||
for _, key := range keys {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
if result := sl.T(key); result == "" {
|
||||
t.Error("key not defined")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,20 +17,6 @@ var (
|
||||
ErrMisconfiguration = errors.New("[unexpected] policy: administrator misconfiguration")
|
||||
)
|
||||
|
||||
type staticHashChecker struct {
|
||||
hash string
|
||||
}
|
||||
|
||||
func (staticHashChecker) Check(r *http.Request) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s staticHashChecker) Hash() string { return s.hash }
|
||||
|
||||
func NewStaticHashChecker(hashable string) checker.Impl {
|
||||
return staticHashChecker{hash: internal.FastHash(hashable)}
|
||||
}
|
||||
|
||||
type RemoteAddrChecker struct {
|
||||
prefixTable *bart.Lite
|
||||
hash string
|
||||
|
||||
@@ -327,7 +327,9 @@ type fileConfig struct {
|
||||
Bots []BotOrImport `json:"bots"`
|
||||
DNSBL bool `json:"dnsbl"`
|
||||
OpenGraph openGraphFileConfig `json:"openGraph,omitempty"`
|
||||
Impressum *Impressum `json:"impressum,omitempty"`
|
||||
StatusCodes StatusCodes `json:"status_codes"`
|
||||
Store *Store `json:"store"`
|
||||
Thresholds []Threshold `json:"thresholds"`
|
||||
}
|
||||
|
||||
@@ -360,6 +362,12 @@ func (c *fileConfig) Valid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Store != nil {
|
||||
if err := c.Store.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
|
||||
}
|
||||
@@ -373,6 +381,9 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
Challenge: http.StatusOK,
|
||||
Deny: http.StatusOK,
|
||||
},
|
||||
Store: &Store{
|
||||
Backend: "memory",
|
||||
},
|
||||
}
|
||||
|
||||
if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&c); err != nil {
|
||||
@@ -391,6 +402,7 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
Override: c.OpenGraph.Override,
|
||||
},
|
||||
StatusCodes: c.StatusCodes,
|
||||
Store: c.Store,
|
||||
}
|
||||
|
||||
if c.OpenGraph.TimeToLive != "" {
|
||||
@@ -421,6 +433,14 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Impressum != nil {
|
||||
if err := c.Impressum.Valid(); err != nil {
|
||||
validationErrs = append(validationErrs, err)
|
||||
}
|
||||
|
||||
result.Impressum = c.Impressum
|
||||
}
|
||||
|
||||
if len(c.Thresholds) == 0 {
|
||||
c.Thresholds = DefaultThresholds
|
||||
}
|
||||
@@ -445,8 +465,10 @@ type Config struct {
|
||||
Bots []BotConfig
|
||||
Thresholds []Threshold
|
||||
DNSBL bool
|
||||
Impressum *Impressum
|
||||
OpenGraph OpenGraph
|
||||
StatusCodes StatusCodes
|
||||
Store *Store
|
||||
}
|
||||
|
||||
func (c Config) Valid() error {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package config
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
. "github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
)
|
||||
|
||||
func p[V any](v V) *V { return &v }
|
||||
@@ -325,37 +326,37 @@ func TestConfigValidBad(t *testing.T) {
|
||||
func TestBotConfigZero(t *testing.T) {
|
||||
var b BotConfig
|
||||
if !b.Zero() {
|
||||
t.Error("zero value BotConfig is not zero value")
|
||||
t.Error("zero value config.BotConfig is not zero value")
|
||||
}
|
||||
|
||||
b.Name = "hi"
|
||||
if b.Zero() {
|
||||
t.Error("BotConfig with name is zero value")
|
||||
t.Error("config.BotConfig with name is zero value")
|
||||
}
|
||||
|
||||
b.UserAgentRegex = p(".*")
|
||||
if b.Zero() {
|
||||
t.Error("BotConfig with user agent regex is zero value")
|
||||
t.Error("config.BotConfig with user agent regex is zero value")
|
||||
}
|
||||
|
||||
b.PathRegex = p(".*")
|
||||
if b.Zero() {
|
||||
t.Error("BotConfig with path regex is zero value")
|
||||
t.Error("config.BotConfig with path regex is zero value")
|
||||
}
|
||||
|
||||
b.HeadersRegex = map[string]string{"hi": "there"}
|
||||
if b.Zero() {
|
||||
t.Error("BotConfig with headers regex is zero value")
|
||||
t.Error("config.BotConfig with headers regex is zero value")
|
||||
}
|
||||
|
||||
b.Action = RuleAllow
|
||||
if b.Zero() {
|
||||
t.Error("BotConfig with action is zero value")
|
||||
t.Error("config.BotConfig with action is zero value")
|
||||
}
|
||||
|
||||
b.RemoteAddr = []string{"::/0"}
|
||||
if b.Zero() {
|
||||
t.Error("BotConfig with remote addresses is zero value")
|
||||
t.Error("config.BotConfig with remote addresses is zero value")
|
||||
}
|
||||
|
||||
b.Challenge = &ChallengeRules{
|
||||
@@ -364,6 +365,6 @@ func TestBotConfigZero(t *testing.T) {
|
||||
Algorithm: DefaultAlgorithm,
|
||||
}
|
||||
if b.Zero() {
|
||||
t.Error("BotConfig with challenge rules is zero value")
|
||||
t.Error("config.BotConfig with challenge rules is zero value")
|
||||
}
|
||||
}
|
||||
|
||||
71
lib/policy/config/impressum.go
Normal file
71
lib/policy/config/impressum.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var ErrMissingValue = errors.New("config: missing value")
|
||||
|
||||
type Impressum struct {
|
||||
Footer string `json:"footer" yaml:"footer"`
|
||||
Page ImpressumPage `json:"page" yaml:"page"`
|
||||
}
|
||||
|
||||
func (i Impressum) Render(_ context.Context, w io.Writer) error {
|
||||
if _, err := fmt.Fprint(w, i.Footer); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i Impressum) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if len(i.Footer) == 0 {
|
||||
errs = append(errs, fmt.Errorf("%w: impressum footer must be defined", ErrMissingValue))
|
||||
}
|
||||
|
||||
if err := i.Page.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImpressumPage struct {
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Body string `json:"body" yaml:"body"`
|
||||
}
|
||||
|
||||
func (ip ImpressumPage) Render(_ context.Context, w io.Writer) error {
|
||||
if _, err := fmt.Fprint(w, ip.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ip ImpressumPage) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if len(ip.Title) == 0 {
|
||||
errs = append(errs, fmt.Errorf("%w: impressum page title must be defined", ErrMissingValue))
|
||||
}
|
||||
|
||||
if len(ip.Body) == 0 {
|
||||
errs = append(errs, fmt.Errorf("%w: impressum body title must be defined", ErrMissingValue))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
lib/policy/config/impressum_test.go
Normal file
62
lib/policy/config/impressum_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestImpressumValid(t *testing.T) {
|
||||
for _, cs := range []struct {
|
||||
name string
|
||||
inp Impressum
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "basic happy path",
|
||||
inp: Impressum{
|
||||
Footer: "<p>Website hosted by Techaro.<p>",
|
||||
Page: ImpressumPage{
|
||||
Title: "Techaro Imprint",
|
||||
Body: "<p>This is an imprint page.</p>",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "no footer",
|
||||
inp: Impressum{
|
||||
Footer: "",
|
||||
Page: ImpressumPage{
|
||||
Title: "Techaro Imprint",
|
||||
Body: "<p>This is an imprint page.</p>",
|
||||
},
|
||||
},
|
||||
err: ErrMissingValue,
|
||||
},
|
||||
{
|
||||
name: "page not valid",
|
||||
inp: Impressum{
|
||||
Footer: "test page please ignore",
|
||||
},
|
||||
err: ErrMissingValue,
|
||||
},
|
||||
} {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
if err := cs.inp.Valid(); !errors.Is(err, cs.err) {
|
||||
t.Logf("want: %v", cs.err)
|
||||
t.Logf("got: %v", err)
|
||||
t.Error("validation failed")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := cs.inp.Render(t.Context(), &buf); err != nil {
|
||||
t.Errorf("can't render footer: %v", err)
|
||||
}
|
||||
|
||||
if err := cs.inp.Page.Render(t.Context(), &buf); err != nil {
|
||||
t.Errorf("can't render page: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
44
lib/policy/config/store.go
Normal file
44
lib/policy/config/store.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/all"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoStoreBackend = errors.New("config.Store: no backend defined")
|
||||
ErrUnknownStoreBackend = errors.New("config.Store: unknown backend")
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
Backend string `json:"backend"`
|
||||
Parameters json.RawMessage `json:"parameters"`
|
||||
}
|
||||
|
||||
func (s *Store) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if len(s.Backend) == 0 {
|
||||
errs = append(errs, ErrNoStoreBackend)
|
||||
}
|
||||
|
||||
fac, ok := store.Get(s.Backend)
|
||||
switch ok {
|
||||
case true:
|
||||
if err := fac.Valid(s.Parameters); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
case false:
|
||||
errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownStoreBackend, s.Backend))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
84
lib/policy/config/store_test.go
Normal file
84
lib/policy/config/store_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/store/bbolt"
|
||||
"github.com/TecharoHQ/anubis/lib/store/valkey"
|
||||
)
|
||||
|
||||
func TestStoreValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
input config.Store
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "no backend",
|
||||
input: config.Store{},
|
||||
err: config.ErrNoStoreBackend,
|
||||
},
|
||||
{
|
||||
name: "in-memory backend",
|
||||
input: config.Store{
|
||||
Backend: "memory",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bbolt backend",
|
||||
input: config.Store{
|
||||
Backend: "bbolt",
|
||||
Parameters: json.RawMessage(`{"path": "/tmp/foo", "bucket": "bar"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valkey backend",
|
||||
input: config.Store{
|
||||
Backend: "valkey",
|
||||
Parameters: json.RawMessage(`{"url": "redis://valkey:6379/0"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valkey backend no URL",
|
||||
input: config.Store{
|
||||
Backend: "valkey",
|
||||
Parameters: json.RawMessage(`{}`),
|
||||
},
|
||||
err: valkey.ErrNoURL,
|
||||
},
|
||||
{
|
||||
name: "valkey backend bad URL",
|
||||
input: config.Store{
|
||||
Backend: "valkey",
|
||||
Parameters: json.RawMessage(`{"url": "http://anubis.techaro.lol"}`),
|
||||
},
|
||||
err: valkey.ErrBadURL,
|
||||
},
|
||||
{
|
||||
name: "bbolt backend no path",
|
||||
input: config.Store{
|
||||
Backend: "bbolt",
|
||||
Parameters: json.RawMessage(`{"path": "", "bucket": "bar"}`),
|
||||
},
|
||||
err: bbolt.ErrMissingPath,
|
||||
},
|
||||
{
|
||||
name: "unknown backend",
|
||||
input: config.Store{
|
||||
Backend: "taco salad",
|
||||
},
|
||||
err: config.ErrUnknownStoreBackend,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||
t.Logf("want: %v", tt.err)
|
||||
t.Logf("got: %v", err)
|
||||
t.Error("invalid error returned")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
lib/policy/config/testdata/bad/impressum-no-footer.yaml
vendored
Normal file
11
lib/policy/config/testdata/bad/impressum-no-footer.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
bots:
|
||||
- name: simple-weight-adjust
|
||||
action: WEIGH
|
||||
user_agent_regex: Mozilla
|
||||
weight:
|
||||
adjust: 5
|
||||
|
||||
impressum:
|
||||
page:
|
||||
title: Test
|
||||
body: <p>This is a test</p>
|
||||
10
lib/policy/config/testdata/bad/impressum-no-page-contents.yaml
vendored
Normal file
10
lib/policy/config/testdata/bad/impressum-no-page-contents.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
bots:
|
||||
- name: simple-weight-adjust
|
||||
action: WEIGH
|
||||
user_agent_regex: Mozilla
|
||||
weight:
|
||||
adjust: 5
|
||||
|
||||
impressum:
|
||||
footer: "Hi there these are WORDS on the INTERNET."
|
||||
page: {}
|
||||
10
lib/policy/config/testdata/good/impressum.yaml
vendored
Normal file
10
lib/policy/config/testdata/good/impressum.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
bots:
|
||||
- name: simple
|
||||
action: CHALLENGE
|
||||
user_agent_regex: Mozilla
|
||||
|
||||
impressum:
|
||||
footer: "Hi these are WORDS on the INTERNET."
|
||||
page:
|
||||
title: Test
|
||||
body: <p>This is a test</p>
|
||||
@@ -11,8 +11,11 @@ import (
|
||||
"github.com/TecharoHQ/anubis/internal/thoth"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/all"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,12 +34,14 @@ type ParsedConfig struct {
|
||||
Bots []Bot
|
||||
Thresholds []*Threshold
|
||||
DNSBL bool
|
||||
Impressum *config.Impressum
|
||||
OpenGraph config.OpenGraph
|
||||
DefaultDifficulty int
|
||||
StatusCodes config.StatusCodes
|
||||
Store store.Interface
|
||||
}
|
||||
|
||||
func NewParsedConfig(orig *config.Config) *ParsedConfig {
|
||||
func newParsedConfig(orig *config.Config) *ParsedConfig {
|
||||
return &ParsedConfig{
|
||||
orig: orig,
|
||||
OpenGraph: orig.OpenGraph,
|
||||
@@ -54,7 +59,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
|
||||
tc, hasThothClient := thoth.FromContext(ctx)
|
||||
|
||||
result := NewParsedConfig(c)
|
||||
result := newParsedConfig(c)
|
||||
result.DefaultDifficulty = defaultDifficulty
|
||||
|
||||
for _, b := range c.Bots {
|
||||
@@ -150,6 +155,8 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
parsedBot.Weight = b.Weight
|
||||
}
|
||||
|
||||
result.Impressum = c.Impressum
|
||||
|
||||
parsedBot.Rules = cl
|
||||
|
||||
result.Bots = append(result.Bots, parsedBot)
|
||||
@@ -175,6 +182,19 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
result.Thresholds = append(result.Thresholds, threshold)
|
||||
}
|
||||
|
||||
stFac, ok := store.Get(c.Store.Backend)
|
||||
switch ok {
|
||||
case true:
|
||||
store, err := stFac.Build(ctx, c.Store.Parameters)
|
||||
if err != nil {
|
||||
validationErrs = append(validationErrs, err)
|
||||
} else {
|
||||
result.Store = store
|
||||
}
|
||||
case false:
|
||||
validationErrs = append(validationErrs, config.ErrUnknownStoreBackend)
|
||||
}
|
||||
|
||||
if len(validationErrs) > 0 {
|
||||
return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...))
|
||||
}
|
||||
|
||||
10
lib/store/all/all.go
Normal file
10
lib/store/all/all.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Package all is a meta-package that imports all store implementations.
|
||||
//
|
||||
// This is a HACK to make tests work consistently.
|
||||
package all
|
||||
|
||||
import (
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/bbolt"
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/memory"
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/valkey"
|
||||
)
|
||||
142
lib/store/bbolt/bbolt.go
Normal file
142
lib/store/bbolt/bbolt.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package bbolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBucketDoesNotExist = errors.New("bbolt: bucket does not exist")
|
||||
ErrNotExists = errors.New("bbolt: value does not exist in store")
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Data []byte `json:"data"`
|
||||
Expires time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
bucket []byte
|
||||
bdb *bbolt.DB
|
||||
}
|
||||
|
||||
func (s *Store) Delete(ctx context.Context, key string) error {
|
||||
return s.bdb.Update(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(s.bucket)
|
||||
if bkt == nil {
|
||||
return fmt.Errorf("%w: %q", ErrBucketDoesNotExist, string(s.bucket))
|
||||
}
|
||||
|
||||
if bkt.Get([]byte(key)) == nil {
|
||||
return fmt.Errorf("%w: %q", ErrNotExists, key)
|
||||
}
|
||||
|
||||
return bkt.Delete([]byte(key))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
var i Item
|
||||
|
||||
if err := s.bdb.View(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(s.bucket)
|
||||
if bkt == nil {
|
||||
return fmt.Errorf("%w: %q", ErrBucketDoesNotExist, string(s.bucket))
|
||||
}
|
||||
|
||||
bucketData := bkt.Get([]byte(key))
|
||||
if bucketData == nil {
|
||||
return fmt.Errorf("%w: %q", store.ErrNotFound, key)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(bucketData, &i); err != nil {
|
||||
return fmt.Errorf("%w: %w", store.ErrCantDecode, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if time.Now().After(i.Expires) {
|
||||
go s.Delete(context.Background(), key)
|
||||
return nil, fmt.Errorf("%w: %q", store.ErrNotFound, key)
|
||||
}
|
||||
|
||||
return i.Data, nil
|
||||
}
|
||||
|
||||
func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
|
||||
i := Item{
|
||||
Data: value,
|
||||
Expires: time.Now().Add(expiry),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", store.ErrCantEncode, err)
|
||||
}
|
||||
|
||||
return s.bdb.Update(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(s.bucket)
|
||||
if bkt == nil {
|
||||
return fmt.Errorf("%w: %q", ErrBucketDoesNotExist, string(s.bucket))
|
||||
}
|
||||
|
||||
return bkt.Put([]byte(key), data)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) cleanup(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
|
||||
return s.bdb.Update(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(s.bucket)
|
||||
if bkt == nil {
|
||||
return fmt.Errorf("cache bucket %q does not exist", string(s.bucket))
|
||||
}
|
||||
|
||||
return bkt.ForEach(func(k, v []byte) error {
|
||||
var i Item
|
||||
|
||||
data := bkt.Get(k)
|
||||
if data == nil {
|
||||
return fmt.Errorf("%s in Cache bucket does not exist???", string(k))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &i); err != nil {
|
||||
return fmt.Errorf("can't unmarshal data at key %s: %w", string(k), err)
|
||||
}
|
||||
|
||||
if now.After(i.Expires) {
|
||||
return bkt.Delete(k)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s *Store) cleanupThread(ctx context.Context) {
|
||||
t := time.NewTicker(5 * time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
if err := s.cleanup(ctx); err != nil {
|
||||
slog.Error("error during bbolt cleanup", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
lib/store/bbolt/bbolt_test.go
Normal file
23
lib/store/bbolt/bbolt_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package bbolt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store/storetest"
|
||||
)
|
||||
|
||||
func TestImpl(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "db")
|
||||
t.Log(path)
|
||||
data, err := json.Marshal(Config{
|
||||
Path: path,
|
||||
Bucket: "anubis",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
storetest.Common(t, Factory{}, json.RawMessage(data))
|
||||
}
|
||||
100
lib/store/bbolt/factory.go
Normal file
100
lib/store/bbolt/factory.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package bbolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingPath = errors.New("bbolt: path is missing from config")
|
||||
ErrCantWriteToPath = errors.New("bbolt: can't write to path")
|
||||
)
|
||||
|
||||
func init() {
|
||||
store.Register("bbolt", Factory{})
|
||||
}
|
||||
|
||||
type Factory struct{}
|
||||
|
||||
func (Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface, error) {
|
||||
var config Config
|
||||
if err := json.Unmarshal([]byte(data), &config); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
if err := config.Valid(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
if config.Bucket == "" {
|
||||
config.Bucket = "anubis"
|
||||
}
|
||||
|
||||
bdb, err := bbolt.Open(config.Path, 0600, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't open bbolt database %s: %w", config.Path, err)
|
||||
}
|
||||
|
||||
if err := bdb.Update(func(tx *bbolt.Tx) error {
|
||||
if _, err := tx.CreateBucketIfNotExists([]byte(config.Bucket)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("can't create bbolt bucket %q: %w", config.Bucket, err)
|
||||
}
|
||||
|
||||
result := &Store{
|
||||
bdb: bdb,
|
||||
bucket: []byte(config.Bucket),
|
||||
}
|
||||
|
||||
go result.cleanupThread(ctx)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (Factory) Valid(data json.RawMessage) error {
|
||||
var config Config
|
||||
if err := json.Unmarshal([]byte(data), &config); err != nil {
|
||||
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
if err := config.Valid(); err != nil {
|
||||
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Path string `json:"path"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
}
|
||||
|
||||
func (c Config) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if c.Path == "" {
|
||||
errs = append(errs, ErrMissingPath)
|
||||
} else {
|
||||
dir := filepath.Dir(c.Path)
|
||||
if err := os.WriteFile(filepath.Join(dir, ".test-file"), []byte(""), 0600); err != nil {
|
||||
errs = append(errs, ErrCantWriteToPath)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
lib/store/bbolt/factory_test.go
Normal file
50
lib/store/bbolt/factory_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package bbolt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFactoryValid(t *testing.T) {
|
||||
f := Factory{}
|
||||
|
||||
t.Run("bad config", func(t *testing.T) {
|
||||
if err := f.Valid(json.RawMessage(`}`)); err == nil {
|
||||
t.Error("wanted parsing failure but got a successful result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid config", func(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
cfg Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "missing path",
|
||||
cfg: Config{},
|
||||
err: ErrMissingPath,
|
||||
},
|
||||
{
|
||||
name: "unwritable folder",
|
||||
cfg: Config{
|
||||
Path: filepath.Join("/", "testdb"),
|
||||
},
|
||||
err: ErrCantWriteToPath,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := json.Marshal(tt.cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := f.Valid(json.RawMessage(data)); !errors.Is(err, tt.err) {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
77
lib/store/interface.go
Normal file
77
lib/store/interface.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound is returned when the store implementation cannot find the value
|
||||
// for a given key.
|
||||
ErrNotFound = errors.New("store: key not found")
|
||||
|
||||
// ErrCantDecode is returned when a store adaptor cannot decode the store format
|
||||
// to a value used by the code.
|
||||
ErrCantDecode = errors.New("store: can't decode value")
|
||||
|
||||
// ErrCantEncode is returned when a store adaptor cannot encode the value into
|
||||
// the format that the store uses.
|
||||
ErrCantEncode = errors.New("store: can't encode value")
|
||||
|
||||
// ErrBadConfig is returned when a store adaptor's configuration is invalid.
|
||||
ErrBadConfig = errors.New("store: configuration is invalid")
|
||||
)
|
||||
|
||||
// Interface defines the calls that Anubis uses for storage in a local or remote
|
||||
// datastore. This can be implemented with an in-memory, on-disk, or in-database
|
||||
// storage backend.
|
||||
type Interface interface {
|
||||
// Delete removes a value from the store by key.
|
||||
Delete(ctx context.Context, key string) error
|
||||
|
||||
// Get returns the value of a key assuming that value exists and has not expired.
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
|
||||
// Set puts a value into the store that expires according to its expiry.
|
||||
Set(ctx context.Context, key string, value []byte, expiry time.Duration) error
|
||||
}
|
||||
|
||||
func z[T any]() T { return *new(T) }
|
||||
|
||||
type JSON[T any] struct {
|
||||
Underlying Interface
|
||||
}
|
||||
|
||||
func (j *JSON[T]) Delete(ctx context.Context, key string) error {
|
||||
return j.Underlying.Delete(ctx, key)
|
||||
}
|
||||
|
||||
func (j *JSON[T]) Get(ctx context.Context, key string) (T, error) {
|
||||
data, err := j.Underlying.Get(ctx, key)
|
||||
if err != nil {
|
||||
return z[T](), err
|
||||
}
|
||||
|
||||
var result T
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return z[T](), fmt.Errorf("%w: %w", ErrCantDecode, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (j *JSON[T]) Set(ctx context.Context, key string, value T, expiry time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrCantEncode, err)
|
||||
}
|
||||
|
||||
if err := j.Underlying.Set(ctx, key, data, expiry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
74
lib/store/memory/memory.go
Normal file
74
lib/store/memory/memory.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/decaymap"
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
)
|
||||
|
||||
type factory struct{}
|
||||
|
||||
func (factory) Build(ctx context.Context, _ json.RawMessage) (store.Interface, error) {
|
||||
return New(ctx), nil
|
||||
}
|
||||
|
||||
func (factory) Valid(json.RawMessage) error { return nil }
|
||||
|
||||
func init() {
|
||||
store.Register("memory", factory{})
|
||||
}
|
||||
|
||||
type impl struct {
|
||||
store *decaymap.Impl[string, []byte]
|
||||
}
|
||||
|
||||
func (i *impl) Delete(_ context.Context, key string) error {
|
||||
if !i.store.Delete(key) {
|
||||
return fmt.Errorf("%w: %q", store.ErrNotFound, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *impl) Get(_ context.Context, key string) ([]byte, error) {
|
||||
result, ok := i.store.Get(key)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %q", store.ErrNotFound, key)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (i *impl) Set(_ context.Context, key string, value []byte, expiry time.Duration) error {
|
||||
i.store.Set(key, value, expiry)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *impl) cleanupThread(ctx context.Context) {
|
||||
t := time.NewTicker(5 * time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
i.store.Cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a simple in-memory store. This will not scale to multiple Anubis instances.
|
||||
func New(ctx context.Context) store.Interface {
|
||||
result := &impl{
|
||||
store: decaymap.New[string, []byte](),
|
||||
}
|
||||
|
||||
go result.cleanupThread(ctx)
|
||||
|
||||
return result
|
||||
}
|
||||
11
lib/store/memory/memory_test.go
Normal file
11
lib/store/memory/memory_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store/storetest"
|
||||
)
|
||||
|
||||
func TestImpl(t *testing.T) {
|
||||
storetest.Common(t, factory{}, nil)
|
||||
}
|
||||
43
lib/store/registry.go
Normal file
43
lib/store/registry.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
registry map[string]Factory = map[string]Factory{}
|
||||
regLock sync.RWMutex
|
||||
)
|
||||
|
||||
type Factory interface {
|
||||
Build(ctx context.Context, config json.RawMessage) (Interface, error)
|
||||
Valid(config json.RawMessage) error
|
||||
}
|
||||
|
||||
func Register(name string, impl Factory) {
|
||||
regLock.Lock()
|
||||
defer regLock.Unlock()
|
||||
|
||||
registry[name] = impl
|
||||
}
|
||||
|
||||
func Get(name string) (Factory, bool) {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
result, ok := registry[name]
|
||||
return result, ok
|
||||
}
|
||||
|
||||
func Methods() []string {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
var result []string
|
||||
for method := range registry {
|
||||
result = append(result, method)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
92
lib/store/storetest/storetest.go
Normal file
92
lib/store/storetest/storetest.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package storetest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
)
|
||||
|
||||
func Common(t *testing.T, f store.Factory, config json.RawMessage) {
|
||||
if err := f.Valid(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s, err := f.Build(t.Context(), config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
doer func(t *testing.T, s store.Interface) error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "basic get set delete",
|
||||
doer: func(t *testing.T, s store.Interface) error {
|
||||
if _, err := s.Get(t.Context(), t.Name()); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Errorf("wanted %s to not exist in store but it exists anyways", t.Name())
|
||||
}
|
||||
|
||||
if err := s.Set(t.Context(), t.Name(), []byte(t.Name()), 5*time.Minute); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := s.Get(t.Context(), t.Name())
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
t.Errorf("wanted %s to exist in store but it does not", t.Name())
|
||||
}
|
||||
|
||||
if !bytes.Equal(val, []byte(t.Name())) {
|
||||
t.Logf("want: %q", t.Name())
|
||||
t.Logf("got: %q", string(val))
|
||||
t.Error("wrong value returned")
|
||||
}
|
||||
|
||||
if err := s.Delete(t.Context(), t.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := s.Get(t.Context(), t.Name()); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Error("wanted test to not exist in store but it exists anyways")
|
||||
}
|
||||
|
||||
if err := s.Delete(t.Context(), t.Name()); err == nil {
|
||||
t.Errorf("key %q does not exist and Delete did not return non-nil", t.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expires",
|
||||
doer: func(t *testing.T, s store.Interface) error {
|
||||
if err := s.Set(t.Context(), t.Name(), []byte(t.Name()), 150*time.Millisecond); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//nosleep:bypass XXX(Xe): use Go's time faking thing in Go 1.25 when that is released.
|
||||
time.Sleep(155 * time.Millisecond)
|
||||
|
||||
if _, err := s.Get(t.Context(), t.Name()); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Errorf("wanted %s to not exist in store but it exists anyways", t.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := tt.doer(t, s); !errors.Is(err, tt.err) {
|
||||
t.Logf("want: %v", tt.err)
|
||||
t.Logf("got: %v", err)
|
||||
t.Error("wrong error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user