Compare commits
78 Commits
Xe/bbolt-c
...
Xe/osiris
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4092180626 | ||
|
|
03758405d3 | ||
|
|
eb78ccc30c | ||
|
|
4156f84020 | ||
|
|
76dcd21582 | ||
|
|
153da4f5ac | ||
|
|
89b6af05a3 | ||
|
|
9a711f1635 | ||
|
|
dabbe63bb6 | ||
|
|
0aed7d3688 | ||
|
|
2af731033c | ||
|
|
6b639cd911 | ||
|
|
a0aba2d74a | ||
|
|
d9c4e37978 | ||
|
|
1eafebedbc | ||
|
|
115ee97d1d | ||
|
|
b485499125 | ||
|
|
300720f030 | ||
|
|
d6298adc6d | ||
|
|
1a9d8fb0cf | ||
|
|
36e25ff5f3 | ||
|
|
c59b7179c3 | ||
|
|
59515ed669 | ||
|
|
4d6b578f93 | ||
|
|
2915c1d209 | ||
|
|
68b653b099 | ||
|
|
509a4f3ce8 | ||
|
|
5c4d8480e6 | ||
|
|
132b2ed853 | ||
|
|
d28991ce8d | ||
|
|
0fd4bb81b8 | ||
|
|
603c68fd54 | ||
|
|
c8f2eb1185 | ||
|
|
f6b94dca98 | ||
|
|
6d8b98eb3d | ||
|
|
b9d8275234 | ||
|
|
c2cc1df172 | ||
|
|
735b2ceb14 | ||
|
|
2cb57fc247 | ||
|
|
61ce581f36 | ||
|
|
3f6750ac7d | ||
|
|
25d75b352a | ||
|
|
de17823bc7 | ||
|
|
29622e605d | ||
|
|
9fa1795db7 | ||
|
|
fbf69680f5 | ||
|
|
c74de19532 | ||
|
|
6dc726013a | ||
|
|
02304e8f3c | ||
|
|
607c9791d8 | ||
|
|
6b67be86a1 | ||
|
|
e02f017153 | ||
|
|
66b39f64af | ||
|
|
944fd25924 | ||
|
|
fa3fbfb0a5 | ||
|
|
3c739c1305 | ||
|
|
cc56baa5c7 | ||
|
|
053d29e0b6 | ||
|
|
a668095c22 | ||
|
|
1c4a1aec4a | ||
|
|
5b8b6d1c94 | ||
|
|
0cb6ef76e1 | ||
|
|
a900e98b8b | ||
|
|
e79cd93b61 | ||
|
|
d17fc6a174 | ||
|
|
95768cb70f | ||
|
|
ca61b8a05f | ||
|
|
1ea1157cd7 | ||
|
|
44ae5f2e2b | ||
|
|
ea2e76c6ee | ||
|
|
4ea0add50d | ||
|
|
289c802a0b | ||
|
|
543b942be1 | ||
|
|
edbe1dcfd6 | ||
|
|
94db16c0df | ||
|
|
c2f46907a1 | ||
|
|
6fa5b8e4e0 | ||
|
|
f98750b038 |
@@ -2,12 +2,15 @@
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
|
||||
{
|
||||
"name": "Dev",
|
||||
"dockerComposeFile": ["./docker-compose.yaml"],
|
||||
"dockerComposeFile": [
|
||||
"./docker-compose.yaml"
|
||||
],
|
||||
"service": "workspace",
|
||||
"workspaceFolder": "/workspace/anubis",
|
||||
"postStartCommand": "npm ci && go mod download",
|
||||
"postStartCommand": "bash ./.devcontainer/poststart.sh",
|
||||
"features": {
|
||||
"ghcr.io/xe/devcontainer-features/ko:1.1.0": {}
|
||||
"ghcr.io/xe/devcontainer-features/ko:1.1.0": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"initializeCommand": "mkdir -p ${localEnv:HOME}${localEnv:USERPROFILE}/.local/share/atuin",
|
||||
"customizations": {
|
||||
@@ -18,8 +21,10 @@
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml"
|
||||
"redhat.vscode-yaml",
|
||||
"hashicorp.hcl",
|
||||
"fredwangwang.vscode-hcl-format"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
.devcontainer/poststart.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
pwd
|
||||
|
||||
npm ci &
|
||||
go mod download &
|
||||
go install ./utils/cmd/... &
|
||||
|
||||
wait
|
||||
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
.env
|
||||
*.deb
|
||||
*.rpm
|
||||
|
||||
# Additional package locks
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Go binaries and test artifacts
|
||||
main
|
||||
*.test
|
||||
|
||||
node_modules
|
||||
|
||||
# MacOS
|
||||
.DS_store
|
||||
|
||||
# Intellij
|
||||
.idea
|
||||
|
||||
# how does this get here
|
||||
doc/VERSION
|
||||
|
||||
web/static/js/*
|
||||
!web/static/js/.gitignore
|
||||
1
.github/actions/spelling/allow.txt
vendored
@@ -3,3 +3,4 @@ https
|
||||
ssh
|
||||
ubuntu
|
||||
workarounds
|
||||
rjack
|
||||
1
.github/actions/spelling/excludes.txt
vendored
@@ -92,3 +92,4 @@ ignore$
|
||||
robots.txt
|
||||
^lib/localization/locales/.*\.json$
|
||||
^lib/localization/.*_test.go$
|
||||
^test/.*$
|
||||
|
||||
28
.github/actions/spelling/expect.txt
vendored
@@ -1,12 +1,10 @@
|
||||
acs
|
||||
aeacus
|
||||
Aibrew
|
||||
alrest
|
||||
amazonbot
|
||||
anthro
|
||||
anubis
|
||||
anubistest
|
||||
apk
|
||||
Applebot
|
||||
archlinux
|
||||
asnc
|
||||
@@ -22,7 +20,6 @@ berr
|
||||
bingbot
|
||||
Bitcoin
|
||||
bitrate
|
||||
blogging
|
||||
Bluesky
|
||||
blueskybot
|
||||
boi
|
||||
@@ -31,6 +28,7 @@ botstopper
|
||||
BPort
|
||||
Brightbot
|
||||
broked
|
||||
byteslice
|
||||
Bytespider
|
||||
cachebuster
|
||||
cachediptoasn
|
||||
@@ -61,7 +59,6 @@ connnection
|
||||
containerbuild
|
||||
coreutils
|
||||
Cotoyogi
|
||||
CRDs
|
||||
Cromite
|
||||
crt
|
||||
Cscript
|
||||
@@ -70,6 +67,7 @@ DDOS
|
||||
Debian
|
||||
debrpm
|
||||
decaymap
|
||||
devcontainers
|
||||
Diffbot
|
||||
discordapp
|
||||
discordbot
|
||||
@@ -119,6 +117,7 @@ goland
|
||||
gomod
|
||||
goodbot
|
||||
googlebot
|
||||
gopsutil
|
||||
govulncheck
|
||||
goyaml
|
||||
GPG
|
||||
@@ -130,6 +129,7 @@ Hashcash
|
||||
hashrate
|
||||
headermap
|
||||
healthcheck
|
||||
healthz
|
||||
hec
|
||||
hmc
|
||||
hostable
|
||||
@@ -145,6 +145,7 @@ Imagesift
|
||||
imgproxy
|
||||
impressum
|
||||
inp
|
||||
internets
|
||||
IPTo
|
||||
iptoasn
|
||||
iss
|
||||
@@ -158,7 +159,6 @@ jshelter
|
||||
JWTs
|
||||
kagi
|
||||
kagibot
|
||||
keikaku
|
||||
Keyfunc
|
||||
keypair
|
||||
KHTML
|
||||
@@ -172,13 +172,13 @@ lgbt
|
||||
licend
|
||||
licstart
|
||||
lightpanda
|
||||
LIMSA
|
||||
limsa
|
||||
Linting
|
||||
linuxbrew
|
||||
LLU
|
||||
loadbalancer
|
||||
lol
|
||||
LOMINSA
|
||||
lominsa
|
||||
maintainership
|
||||
malware
|
||||
mcr
|
||||
@@ -186,12 +186,13 @@ memes
|
||||
metarefresh
|
||||
metrix
|
||||
mimi
|
||||
minica
|
||||
Minfilia
|
||||
mistralai
|
||||
Mojeek
|
||||
mojeekbot
|
||||
mozilla
|
||||
nbf
|
||||
nepeat
|
||||
netsurf
|
||||
nginx
|
||||
nicksnyder
|
||||
@@ -220,6 +221,7 @@ pipefail
|
||||
pki
|
||||
podkova
|
||||
podman
|
||||
poststart
|
||||
prebaked
|
||||
privkey
|
||||
promauto
|
||||
@@ -239,9 +241,7 @@ redhat
|
||||
redir
|
||||
redirectscheme
|
||||
refactors
|
||||
relayd
|
||||
reputational
|
||||
reqmeta
|
||||
risc
|
||||
ruleset
|
||||
runlevels
|
||||
@@ -253,11 +253,11 @@ searchbot
|
||||
searx
|
||||
sebest
|
||||
secretplans
|
||||
selfsigned
|
||||
Semrush
|
||||
Seo
|
||||
setsebool
|
||||
shellcheck
|
||||
shirou
|
||||
Sidetrade
|
||||
simprint
|
||||
sitemap
|
||||
@@ -295,10 +295,8 @@ uberspace
|
||||
Unbreak
|
||||
unbreakdocker
|
||||
unifiedjs
|
||||
unixhttpd
|
||||
unmarshal
|
||||
unparseable
|
||||
uuidgen
|
||||
uvx
|
||||
UXP
|
||||
valkey
|
||||
@@ -307,6 +305,8 @@ Velen
|
||||
vendored
|
||||
vhosts
|
||||
videotest
|
||||
VKE
|
||||
Vultr
|
||||
waitloop
|
||||
weblate
|
||||
webmaster
|
||||
@@ -320,7 +320,6 @@ wordpress
|
||||
Workaround
|
||||
workdir
|
||||
wpbot
|
||||
xcaddy
|
||||
Xeact
|
||||
xeiaso
|
||||
xeserv
|
||||
@@ -337,6 +336,7 @@ yeet
|
||||
yeetfile
|
||||
yourdomain
|
||||
yoursite
|
||||
yyz
|
||||
Zenos
|
||||
zizmor
|
||||
zombocom
|
||||
|
||||
29
.github/workflows/docker-pr.yml
vendored
@@ -2,7 +2,7 @@ name: Docker image builds (pull requests)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
|
||||
env:
|
||||
DOCKER_METADATA_SET_OUTPUT_ENV: "true"
|
||||
@@ -11,7 +11,32 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
buildx-bake:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
|
||||
with:
|
||||
source: .
|
||||
push: true
|
||||
sbom: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
set: |
|
||||
osiris.tags=ttl.sh/techaro/pr-${{ github.event.number }}/osiris:24h
|
||||
|
||||
containerbuild:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
33
.github/workflows/docker.yml
vendored
@@ -17,7 +17,38 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
buildx-bake:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
|
||||
with:
|
||||
source: .
|
||||
push: true
|
||||
sbom: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
set: ""
|
||||
|
||||
containerbuild:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
11
.github/workflows/docs-deploy.yml
vendored
@@ -36,6 +36,9 @@ jobs:
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/techarohq/anubis/docs
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
main
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
@@ -49,15 +52,15 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
|
||||
- name: Apply k8s manifests to aeacus
|
||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
args: apply -k docs/manifest
|
||||
|
||||
- name: Apply k8s manifests to aeacus
|
||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
|
||||
7
.github/workflows/docs-test.yml
vendored
@@ -2,7 +2,7 @@ name: Docs test build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -24,7 +24,10 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/docs
|
||||
images: ghcr.io/techarohq/anubis/docs
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
main
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
|
||||
2
.github/workflows/go.yml
vendored
@@ -82,7 +82,7 @@ jobs:
|
||||
run: npm run test
|
||||
|
||||
- name: Lint with staticcheck
|
||||
uses: dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6 # v1.3.1
|
||||
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
|
||||
117
.github/workflows/package-builds-stable.yml
vendored
@@ -1,8 +1,9 @@
|
||||
name: Package builds (stable)
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
# release:
|
||||
# types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -13,67 +14,67 @@ jobs:
|
||||
#runs-on: alrest-techarohq
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: build essential
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential
|
||||
- name: build essential
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential
|
||||
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
/home/linuxbrew/.linuxbrew/bin
|
||||
/home/linuxbrew/.linuxbrew/etc
|
||||
/home/linuxbrew/.linuxbrew/include
|
||||
/home/linuxbrew/.linuxbrew/lib
|
||||
/home/linuxbrew/.linuxbrew/opt
|
||||
/home/linuxbrew/.linuxbrew/sbin
|
||||
/home/linuxbrew/.linuxbrew/share
|
||||
/home/linuxbrew/.linuxbrew/var
|
||||
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-homebrew-cellar-
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
/home/linuxbrew/.linuxbrew/bin
|
||||
/home/linuxbrew/.linuxbrew/etc
|
||||
/home/linuxbrew/.linuxbrew/include
|
||||
/home/linuxbrew/.linuxbrew/lib
|
||||
/home/linuxbrew/.linuxbrew/opt
|
||||
/home/linuxbrew/.linuxbrew/sbin
|
||||
/home/linuxbrew/.linuxbrew/share
|
||||
/home/linuxbrew/.linuxbrew/var
|
||||
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-homebrew-cellar-
|
||||
|
||||
- name: Install Brew dependencies
|
||||
run: |
|
||||
brew bundle
|
||||
- name: Install Brew dependencies
|
||||
run: |
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-golang-
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-golang-
|
||||
|
||||
- name: install node deps
|
||||
run: |
|
||||
npm ci
|
||||
- name: install node deps
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Build Packages
|
||||
run: |
|
||||
go tool yeet
|
||||
- name: Build Packages
|
||||
run: |
|
||||
go tool yeet
|
||||
|
||||
- name: Upload released artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.TOKEN }}
|
||||
RELEASE_VERSION: ${{github.event.release.tag_name}}
|
||||
shell: bash
|
||||
run: |
|
||||
RELEASE="${RELEASE_VERSION}"
|
||||
cd var
|
||||
for file in *; do
|
||||
gh release upload $RELEASE $file
|
||||
done
|
||||
- name: Upload released artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.TOKEN }}
|
||||
RELEASE_VERSION: ${{github.event.release.tag_name}}
|
||||
shell: bash
|
||||
run: |
|
||||
RELEASE="${RELEASE_VERSION}"
|
||||
cd var
|
||||
for file in *; do
|
||||
gh release upload $RELEASE $file
|
||||
done
|
||||
|
||||
45
.github/workflows/smoke-tests.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Smoke tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
strategy:
|
||||
matrix:
|
||||
test:
|
||||
- git-clone
|
||||
- git-push
|
||||
- healthcheck
|
||||
- i18n
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
||||
|
||||
- name: Install utils
|
||||
run: |
|
||||
go install ./utils/cmd/...
|
||||
|
||||
- name: Run test
|
||||
run: |
|
||||
cd test/${{ matrix.test }}
|
||||
backoff-retry --try-count 10 ./test.sh
|
||||
8
.github/workflows/ssh-ci.yml
vendored
@@ -25,13 +25,19 @@ jobs:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install CI target SSH key
|
||||
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
|
||||
with:
|
||||
key: ${{ secrets.CI_SSH_KEY }}
|
||||
name: id_rsa
|
||||
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
|
||||
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Run CI
|
||||
run: bash test/ssh-ci/rigging.sh ${{ matrix.host }}
|
||||
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
|
||||
env:
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
|
||||
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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.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@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1
|
||||
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
4
.vscode/extensions.json
vendored
@@ -5,6 +5,8 @@
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml"
|
||||
"redhat.vscode-yaml",
|
||||
"hashicorp.hcl",
|
||||
"fredwangwang.vscode-hcl-format"
|
||||
]
|
||||
}
|
||||
14
README.md
@@ -41,6 +41,20 @@ Anubis is brought to you by sponsors and donors like:
|
||||
<a href="https://wildbase.xyz/">
|
||||
<img src="./docs/static/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64">
|
||||
</a>
|
||||
<a href="https://emma.pet">
|
||||
<img
|
||||
src="./docs/static/img/sponsors/nepeat-logo.webp"
|
||||
alt="Cat eyes over the word Emma in a serif font"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://fabulous.systems/">
|
||||
<img
|
||||
src="./docs/static/img/sponsors/fabulous-systems.webp"
|
||||
alt="Cat eyes over the word Emma in a serif font"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"github.com/facebookgo/flagenv"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -97,7 +98,7 @@ func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
||||
}
|
||||
|
||||
func doHealthCheck() error {
|
||||
resp, err := http.Get("http://localhost" + *metricsBind + anubis.BasePrefix + "/metrics")
|
||||
resp, err := http.Get("http://localhost" + *metricsBind + "/healthz")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch metrics: %w", err)
|
||||
}
|
||||
@@ -241,6 +242,15 @@ func main() {
|
||||
}
|
||||
|
||||
internal.InitSlog(*slogLevel)
|
||||
internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||
|
||||
if *healthcheck {
|
||||
log.Println("running healthcheck")
|
||||
if err := doHealthCheck(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if *extractResources != "" {
|
||||
if err := extractEmbedFS(data.BotPolicies, ".", *extractResources); err != nil {
|
||||
@@ -253,6 +263,17 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// install signal handler
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
if *metricsBind != "" {
|
||||
wg.Add(1)
|
||||
go metricsServer(ctx, wg.Done)
|
||||
}
|
||||
|
||||
var rp http.Handler
|
||||
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
|
||||
if strings.TrimSpace(*target) != "" {
|
||||
@@ -267,8 +288,6 @@ func main() {
|
||||
log.Fatalf("you can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN at the same time")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Thoth configuration
|
||||
switch {
|
||||
case *thothURL != "" && *thothToken == "":
|
||||
@@ -398,21 +417,12 @@ func main() {
|
||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
}
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
// install signal handler
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if *metricsBind != "" {
|
||||
wg.Add(1)
|
||||
go metricsServer(ctx, wg.Done)
|
||||
}
|
||||
|
||||
var h http.Handler
|
||||
h = s
|
||||
h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
|
||||
h = internal.XForwardedForToXRealIP(h)
|
||||
h = internal.XForwardedForUpdate(*xffStripPrivate, h)
|
||||
h = internal.JA4H(h)
|
||||
|
||||
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
||||
@@ -441,6 +451,8 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
internal.SetHealth("anubis", healthv1.HealthCheckResponse_SERVING)
|
||||
|
||||
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -451,20 +463,30 @@ func metricsServer(ctx context.Context, done func()) {
|
||||
defer done()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(anubis.BasePrefix+"/metrics", promhttp.Handler())
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
st, ok := internal.GetHealth("anubis")
|
||||
if !ok {
|
||||
slog.Error("health service anubis does not exist, file a bug")
|
||||
}
|
||||
|
||||
switch st {
|
||||
case healthv1.HealthCheckResponse_NOT_SERVING:
|
||||
http.Error(w, "NOT OK", http.StatusInternalServerError)
|
||||
return
|
||||
case healthv1.HealthCheckResponse_SERVING:
|
||||
fmt.Fprintln(w, "OK")
|
||||
return
|
||||
default:
|
||||
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
|
||||
slog.Debug("listening for metrics", "url", metricsUrl)
|
||||
|
||||
if *healthcheck {
|
||||
log.Println("running healthcheck")
|
||||
if err := doHealthCheck(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
||||
39
cmd/osiris/internal/config/bind.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidHostpost = errors.New("bind: invalid host:port")
|
||||
)
|
||||
|
||||
type Bind struct {
|
||||
HTTP string `hcl:"http"`
|
||||
HTTPS string `hcl:"https"`
|
||||
Metrics string `hcl:"metrics"`
|
||||
}
|
||||
|
||||
func (b *Bind) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if _, _, err := net.SplitHostPort(b.HTTP); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.HTTP, err))
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(b.HTTPS); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.HTTPS, err))
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(b.Metrics); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.Metrics, err))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
55
cmd/osiris/internal/config/bind_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBindValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
precondition func(t *testing.T)
|
||||
bind Bind
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
precondition: nil,
|
||||
bind: Bind{
|
||||
HTTP: ":8081",
|
||||
HTTPS: ":8082",
|
||||
Metrics: ":8083",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid ports",
|
||||
precondition: func(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", ":8081")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
},
|
||||
bind: Bind{
|
||||
HTTP: "",
|
||||
HTTPS: "",
|
||||
Metrics: "",
|
||||
},
|
||||
err: ErrInvalidHostpost,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.precondition != nil {
|
||||
tt.precondition(t)
|
||||
}
|
||||
|
||||
if err := tt.bind.Valid(); !errors.Is(err, tt.err) {
|
||||
t.Logf("want: %v", tt.err)
|
||||
t.Logf("got: %v", err)
|
||||
t.Error("got wrong error from validation function")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
31
cmd/osiris/internal/config/config.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Toplevel struct {
|
||||
Bind Bind `hcl:"bind,block"`
|
||||
Domains []Domain `hcl:"domain,block"`
|
||||
}
|
||||
|
||||
func (t *Toplevel) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if err := t.Bind.Valid(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("invalid bind block:\n%w", err))
|
||||
}
|
||||
|
||||
for _, d := range t.Domains {
|
||||
if err := d.Valid(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("when parsing domain %s: %w", d.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("invalid configuration file:\n%w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
cmd/osiris/internal/config/domain.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDomainName = errors.New("domain: name is invalid")
|
||||
ErrInvalidDomainTLSConfig = errors.New("domain: TLS config is invalid")
|
||||
ErrInvalidURL = errors.New("invalid URL")
|
||||
ErrInvalidURLScheme = errors.New("URL has invalid scheme")
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
Name string `hcl:"name,label"`
|
||||
TLS TLS `hcl:"tls,block"`
|
||||
Target string `hcl:"target"`
|
||||
InsecureSkipVerify bool `hcl:"insecure_skip_verify,optional"`
|
||||
HealthTarget string `hcl:"health_target"`
|
||||
}
|
||||
|
||||
func (d Domain) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if _, err := idna.Lookup.ToASCII(d.Name); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidDomainName, d.Name, err))
|
||||
}
|
||||
|
||||
if err := d.TLS.Valid(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidDomainTLSConfig, err))
|
||||
}
|
||||
|
||||
if err := isURLValid(d.Target); err != nil {
|
||||
errs = append(errs, fmt.Errorf("target has %w %q: %w", ErrInvalidURL, d.Target, err))
|
||||
}
|
||||
|
||||
if err := isURLValid(d.HealthTarget); err != nil {
|
||||
errs = append(errs, fmt.Errorf("health_target has %w %q: %w", ErrInvalidURL, d.HealthTarget, err))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isURLValid(input string) error {
|
||||
u, err := url.Parse(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http", "https", "h2c", "unix":
|
||||
// do nothing
|
||||
default:
|
||||
return fmt.Errorf("%w %s has scheme %s (want http, https, h2c, unix)", ErrInvalidURLScheme, input, u.Scheme)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
89
cmd/osiris/internal/config/domain_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDomainValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
input Domain
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "simple happy path",
|
||||
input: Domain{
|
||||
Name: "anubis.techaro.lol",
|
||||
TLS: TLS{
|
||||
Cert: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
},
|
||||
Target: "http://localhost:3000",
|
||||
HealthTarget: "http://localhost:9091/healthz",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid domain name",
|
||||
input: Domain{
|
||||
Name: "\uFFFD.techaro.lol",
|
||||
TLS: TLS{
|
||||
Cert: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
},
|
||||
Target: "http://localhost:3000",
|
||||
HealthTarget: "http://localhost:9091/healthz",
|
||||
},
|
||||
err: ErrInvalidDomainName,
|
||||
},
|
||||
{
|
||||
name: "invalid tls config",
|
||||
input: Domain{
|
||||
Name: "anubis.techaro.lol",
|
||||
TLS: TLS{
|
||||
Cert: "./testdata/tls/invalid.crt",
|
||||
Key: "./testdata/tls/invalid.key",
|
||||
},
|
||||
Target: "http://localhost:3000",
|
||||
HealthTarget: "http://localhost:9091/healthz",
|
||||
},
|
||||
err: ErrInvalidDomainTLSConfig,
|
||||
},
|
||||
{
|
||||
name: "invalid URL",
|
||||
input: Domain{
|
||||
Name: "anubis.techaro.lol",
|
||||
TLS: TLS{
|
||||
Cert: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
},
|
||||
Target: "file://[::1:3000",
|
||||
HealthTarget: "file://[::1:9091/healthz",
|
||||
},
|
||||
err: ErrInvalidURL,
|
||||
},
|
||||
{
|
||||
name: "wrong URL scheme",
|
||||
input: Domain{
|
||||
Name: "anubis.techaro.lol",
|
||||
TLS: TLS{
|
||||
Cert: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
},
|
||||
Target: "file://localhost:3000",
|
||||
HealthTarget: "file://localhost:9091/healthz",
|
||||
},
|
||||
err: ErrInvalidURLScheme,
|
||||
},
|
||||
} {
|
||||
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("got wrong error from validation function")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1
cmd/osiris/internal/config/testdata/tls/invalid.crt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
|
||||
1
cmd/osiris/internal/config/testdata/tls/invalid.key
vendored
Normal file
@@ -0,0 +1 @@
|
||||
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
|
||||
11
cmd/osiris/internal/config/testdata/tls/selfsigned.crt
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnzCCAVGgAwIBAgIUAw8funCpiB3ZAAPoWdSCWnzbsFIwBQYDK2VwMEUxCzAJ
|
||||
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MTkwMjM1WhcNMjUwODE3MTkwMjM1
|
||||
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAcXDHXV3vgpvjtTaz
|
||||
s0Oj/73rMr06bhyGGhleYS1MNoWjUzBRMB0GA1UdDgQWBBQwmfKPthucFHB6Wfgz
|
||||
2Nj5nkMQOjAfBgNVHSMEGDAWgBQwmfKPthucFHB6Wfgz2Nj5nkMQOjAPBgNVHRMB
|
||||
Af8EBTADAQH/MAUGAytlcANBALBYbULlGwB7Ro0UTgUoQDNxEvayn3qzVFHIt7lC
|
||||
/2/NzNBkk4yPT+a4mbRuydxLkv+JIvmQbarZxpksYnWlCAM=
|
||||
-----END CERTIFICATE-----
|
||||
3
cmd/osiris/internal/config/testdata/tls/selfsigned.key
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIOHKoX22Mha6SnnpLm34fSSfTUDbRiDCi6N1nOgTOlds
|
||||
-----END PRIVATE KEY-----
|
||||
40
cmd/osiris/internal/config/tls.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCantReadTLS = errors.New("tls: can't read TLS")
|
||||
ErrInvalidTLSKeypair = errors.New("tls: can't parse TLS keypair")
|
||||
)
|
||||
|
||||
type TLS struct {
|
||||
Cert string `hcl:"cert"`
|
||||
Key string `hcl:"key"`
|
||||
}
|
||||
|
||||
func (t TLS) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if _, err := os.Stat(t.Cert); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w certificate %s: %w", ErrCantReadTLS, t.Cert, err))
|
||||
}
|
||||
|
||||
if _, err := os.Stat(t.Key); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w key %s: %w", ErrCantReadTLS, t.Key, err))
|
||||
}
|
||||
|
||||
if _, err := tls.LoadX509KeyPair(t.Cert, t.Key); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w (%s, %s): %w", ErrInvalidTLSKeypair, t.Cert, t.Key, err))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
48
cmd/osiris/internal/config/tls_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTLSValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
input TLS
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "simple selfsigned",
|
||||
input: TLS{
|
||||
Cert: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files don't exist",
|
||||
input: TLS{
|
||||
Cert: "./testdata/tls/nonexistent.crt",
|
||||
Key: "./testdata/tls/nonexistent.key",
|
||||
},
|
||||
err: ErrCantReadTLS,
|
||||
},
|
||||
{
|
||||
name: "invalid keypair",
|
||||
input: TLS{
|
||||
Cert: "./testdata/tls/invalid.crt",
|
||||
Key: "./testdata/tls/invalid.key",
|
||||
},
|
||||
err: ErrInvalidTLSKeypair,
|
||||
},
|
||||
} {
|
||||
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("got wrong error from validation function")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
85
cmd/osiris/internal/entrypoint/entrypoint.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
|
||||
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||
"golang.org/x/sync/errgroup"
|
||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
ConfigFname string
|
||||
}
|
||||
|
||||
func Main(ctx context.Context, opts Options) error {
|
||||
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||
|
||||
var cfg config.Toplevel
|
||||
if err := hclsimple.DecodeFile(opts.ConfigFname, nil, &cfg); err != nil {
|
||||
return fmt.Errorf("can't read configuration file %s:\n\n%w", opts.ConfigFname, err)
|
||||
}
|
||||
|
||||
if err := cfg.Valid(); err != nil {
|
||||
return fmt.Errorf("configuration file %s is invalid:\n\n%w", opts.ConfigFname, err)
|
||||
}
|
||||
|
||||
rtr, err := NewRouter(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rtr.opts = opts
|
||||
go rtr.backgroundReloadConfig(ctx)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
// HTTP
|
||||
g.Go(func() error {
|
||||
ln, err := net.Listen("tcp", cfg.Bind.HTTP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("(HTTP) can't bind to tcp %s: %w", cfg.Bind.HTTP, err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
go func(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
}(ctx)
|
||||
|
||||
slog.Info("listening", "for", "http", "bind", cfg.Bind.HTTP)
|
||||
|
||||
return rtr.HandleHTTP(gCtx, ln)
|
||||
})
|
||||
|
||||
// HTTPS
|
||||
g.Go(func() error {
|
||||
ln, err := net.Listen("tcp", cfg.Bind.HTTPS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("(https) can't bind to tcp %s: %w", cfg.Bind.HTTPS, err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
go func(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
}(ctx)
|
||||
|
||||
slog.Info("listening", "for", "https", "bind", cfg.Bind.HTTPS)
|
||||
|
||||
return rtr.HandleHTTPS(gCtx, ln)
|
||||
})
|
||||
|
||||
// Metrics
|
||||
g.Go(func() error {
|
||||
return rtr.ListenAndServeMetrics(gCtx, cfg.Bind.Metrics)
|
||||
})
|
||||
|
||||
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
93
cmd/osiris/internal/entrypoint/entrypoint_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMainGoodConfig(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/good")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, st := range files {
|
||||
t.Run(st.Name(), func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cfg := loadConfig(t, filepath.Join("testdata", "good", st.Name()))
|
||||
|
||||
go func(ctx context.Context) {
|
||||
if err := Main(ctx, Options{
|
||||
ConfigFname: filepath.Join("testdata", "good", st.Name()),
|
||||
}); err != nil {
|
||||
var netOpErr *net.OpError
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
// Context was canceled, this is expected
|
||||
return
|
||||
case errors.As(err, &netOpErr):
|
||||
// Network operation error occurred
|
||||
t.Logf("Network operation error: %v", netOpErr)
|
||||
return
|
||||
case errors.Is(err, http.ErrServerClosed):
|
||||
// Server was closed, this is expected
|
||||
return
|
||||
default:
|
||||
// Other unexpected error
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
wait := 5 * time.Millisecond
|
||||
|
||||
for i := range make([]struct{}, 10) {
|
||||
if i != 0 {
|
||||
time.Sleep(wait)
|
||||
wait = wait * 2
|
||||
}
|
||||
|
||||
t.Logf("try %d (wait=%s)", i+1, wait)
|
||||
|
||||
resp, err := http.Get("http://localhost" + cfg.Bind.Metrics + "/readyz")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
continue
|
||||
}
|
||||
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatal("router initialization did not work")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainBadConfig(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/bad")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, st := range files {
|
||||
t.Run(st.Name(), func(t *testing.T) {
|
||||
if err := Main(t.Context(), Options{
|
||||
ConfigFname: filepath.Join("testdata", "bad", st.Name()),
|
||||
}); err == nil {
|
||||
t.Error("wanted an error but got none")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
35
cmd/osiris/internal/entrypoint/h2c.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func newH2CReverseProxy(target *url.URL) *httputil.ReverseProxy {
|
||||
target.Scheme = "http"
|
||||
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
}
|
||||
|
||||
// Use h2c transport
|
||||
transport := &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
// Just do plain TCP (h2c)
|
||||
return net.Dial(network, addr)
|
||||
},
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{
|
||||
Director: director,
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
51
cmd/osiris/internal/entrypoint/h2c_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func newH2cServer(t *testing.T, h http.Handler) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
h2s := &http2.Server{}
|
||||
|
||||
srv := httptest.NewServer(h2c.NewHandler(h, h2s))
|
||||
t.Cleanup(func() {
|
||||
srv.Close()
|
||||
})
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func TestH2CReverseProxy(t *testing.T) {
|
||||
h := &ackHandler{}
|
||||
|
||||
srv := newH2cServer(t, h)
|
||||
|
||||
u, err := url.Parse(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rp := httptest.NewServer(newH2CReverseProxy(u))
|
||||
defer rp.Close()
|
||||
|
||||
resp, err := rp.Client().Get(rp.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("wrong status code from reverse proxy: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if !h.ack {
|
||||
t.Error("h2c handler was not executed")
|
||||
}
|
||||
}
|
||||
72
cmd/osiris/internal/entrypoint/metrics.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
func healthz(w http.ResponseWriter, r *http.Request) {
|
||||
services, err := internal.HealthSrv.List(r.Context(), nil)
|
||||
if err != nil {
|
||||
slog.Error("can't get list of services", "err", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range services.Statuses {
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
var msg bytes.Buffer
|
||||
|
||||
var healthy bool = true
|
||||
|
||||
for _, k := range keys {
|
||||
st := services.Statuses[k].GetStatus()
|
||||
fmt.Fprintf(&msg, "%s: %s\n", k, st)
|
||||
switch st {
|
||||
case healthv1.HealthCheckResponse_SERVING:
|
||||
// do nothing
|
||||
default:
|
||||
healthy = false
|
||||
}
|
||||
}
|
||||
|
||||
if !healthy {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Write(msg.Bytes())
|
||||
}
|
||||
|
||||
func readyz(w http.ResponseWriter, r *http.Request) {
|
||||
st, ok := internal.GetHealth("osiris")
|
||||
if !ok {
|
||||
slog.Error("health service osiris does not exist, file a bug")
|
||||
http.Error(w, "health service osiris does not exist", http.StatusExpectationFailed)
|
||||
}
|
||||
|
||||
switch st {
|
||||
case healthv1.HealthCheckResponse_NOT_SERVING:
|
||||
http.Error(w, "NOT OK", http.StatusInternalServerError)
|
||||
return
|
||||
case healthv1.HealthCheckResponse_SERVING:
|
||||
fmt.Fprintln(w, "OK")
|
||||
return
|
||||
default:
|
||||
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
|
||||
return
|
||||
}
|
||||
}
|
||||
66
cmd/osiris/internal/entrypoint/metrics_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(healthz))
|
||||
|
||||
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||
|
||||
resp, err := srv.Client().Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
t.Errorf("wanted not ready but got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
|
||||
|
||||
resp, err = srv.Client().Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("wanted ready but got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyz(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(readyz))
|
||||
|
||||
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||
|
||||
resp, err := srv.Client().Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
t.Errorf("wanted not ready but got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
|
||||
|
||||
resp, err = srv.Client().Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("wanted ready but got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
320
cmd/osiris/internal/entrypoint/router.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/fingerprint"
|
||||
"github.com/felixge/httpsnoop"
|
||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||
"github.com/lum8rjack/go-ja4h"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTargetInvalid = errors.New("[unexpected] target invalid")
|
||||
ErrNoHandler = errors.New("[unexpected] no handler for domain")
|
||||
ErrInvalidTLSKeypair = errors.New("[unexpected] invalid TLS keypair")
|
||||
ErrNoCert = errors.New("this server does not have a certificate for that domain")
|
||||
|
||||
requestsPerDomain = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "techaro",
|
||||
Subsystem: "osiris",
|
||||
Name: "request_count",
|
||||
}, []string{"domain", "method", "response_code"})
|
||||
|
||||
responseTime = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "techaro",
|
||||
Subsystem: "osiris",
|
||||
Name: "response_time",
|
||||
}, []string{"domain"})
|
||||
|
||||
unresolvedRequests = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "techaro",
|
||||
Subsystem: "osiris",
|
||||
Name: "unresolved_requests",
|
||||
})
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
lock sync.RWMutex
|
||||
routes map[string]http.Handler
|
||||
tlsCerts map[string]*tls.Certificate
|
||||
opts Options
|
||||
}
|
||||
|
||||
func (rtr *Router) setConfig(c config.Toplevel) error {
|
||||
var errs []error
|
||||
newMap := map[string]http.Handler{}
|
||||
newCerts := map[string]*tls.Certificate{}
|
||||
|
||||
for _, d := range c.Domains {
|
||||
var domainErrs []error
|
||||
|
||||
u, err := url.Parse(d.Target)
|
||||
if err != nil {
|
||||
domainErrs = append(domainErrs, fmt.Errorf("%w %q: %v", ErrTargetInvalid, d.Target, err))
|
||||
}
|
||||
|
||||
var h http.Handler
|
||||
|
||||
if u != nil {
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
rp := httputil.NewSingleHostReverseProxy(u)
|
||||
|
||||
if d.InsecureSkipVerify {
|
||||
rp.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
h = rp
|
||||
case "h2c":
|
||||
h = newH2CReverseProxy(u)
|
||||
case "unix":
|
||||
h = &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
r.URL.Scheme = "http"
|
||||
r.URL.Host = d.Name
|
||||
r.Host = d.Name
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", strings.TrimPrefix(d.Target, "unix://"))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if h == nil {
|
||||
domainErrs = append(domainErrs, ErrNoHandler)
|
||||
}
|
||||
|
||||
newMap[d.Name] = h
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(d.TLS.Cert, d.TLS.Key)
|
||||
if err != nil {
|
||||
domainErrs = append(domainErrs, fmt.Errorf("%w: %w", ErrInvalidTLSKeypair, err))
|
||||
}
|
||||
|
||||
newCerts[d.Name] = &cert
|
||||
|
||||
if len(domainErrs) != 0 {
|
||||
errs = append(errs, fmt.Errorf("invalid domain %s: %w", d.Name, errors.Join(domainErrs...)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("can't compile config to routing map: %w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
rtr.lock.Lock()
|
||||
rtr.routes = newMap
|
||||
rtr.tlsCerts = newCerts
|
||||
rtr.lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rtr *Router) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
rtr.lock.RLock()
|
||||
cert, ok := rtr.tlsCerts[hello.ServerName]
|
||||
rtr.lock.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil, ErrNoCert
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (rtr *Router) loadConfig() error {
|
||||
slog.Info("reloading config", "fname", rtr.opts.ConfigFname)
|
||||
var cfg config.Toplevel
|
||||
if err := hclsimple.DecodeFile(rtr.opts.ConfigFname, nil, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cfg.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := rtr.setConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Info("done!")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rtr *Router) backgroundReloadConfig(ctx context.Context) {
|
||||
t := time.NewTicker(time.Hour)
|
||||
defer t.Stop()
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGHUP)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
if err := rtr.loadConfig(); err != nil {
|
||||
slog.Error("can't reload config", "fname", rtr.opts.ConfigFname, "err", err)
|
||||
}
|
||||
case <-ch:
|
||||
if err := rtr.loadConfig(); err != nil {
|
||||
slog.Error("can't reload config", "fname", rtr.opts.ConfigFname, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewRouter(c config.Toplevel) (*Router, error) {
|
||||
result := &Router{
|
||||
routes: map[string]http.Handler{},
|
||||
}
|
||||
|
||||
if err := result.setConfig(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (rtr *Router) HandleHTTP(ctx context.Context, ln net.Listener) error {
|
||||
srv := http.Server{
|
||||
Handler: rtr,
|
||||
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||
}
|
||||
|
||||
go func(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
srv.Close()
|
||||
}(ctx)
|
||||
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
|
||||
func (rtr *Router) HandleHTTPS(ctx context.Context, ln net.Listener) error {
|
||||
tc := &tls.Config{
|
||||
GetCertificate: rtr.GetCertificate,
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: rtr,
|
||||
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||
TLSConfig: tc,
|
||||
}
|
||||
|
||||
go func(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
srv.Close()
|
||||
}(ctx)
|
||||
|
||||
fingerprint.ApplyTLSFingerprinter(srv)
|
||||
|
||||
return srv.ServeTLS(ln, "", "")
|
||||
}
|
||||
|
||||
func (rtr *Router) ListenAndServeMetrics(ctx context.Context, addr string) error {
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("(metrics) can't bind to tcp %s: %w", addr, err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
go func(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
}(ctx)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/readyz", readyz)
|
||||
mux.HandleFunc("/healthz", healthz)
|
||||
|
||||
slog.Info("listening", "for", "metrics", "bind", addr)
|
||||
|
||||
srv := http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||
}
|
||||
|
||||
go func(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
srv.Close()
|
||||
}(ctx)
|
||||
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
|
||||
func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var host = r.Host
|
||||
|
||||
if strings.Contains(host, ":") {
|
||||
host, _, _ = net.SplitHostPort(host)
|
||||
}
|
||||
|
||||
var h http.Handler
|
||||
var ok bool
|
||||
|
||||
ja4hFP := ja4h.JA4H(r)
|
||||
|
||||
slog.Info("got request", "method", r.Method, "host", host, "path", r.URL.Path)
|
||||
|
||||
rtr.lock.RLock()
|
||||
h, ok = rtr.routes[host]
|
||||
rtr.lock.RUnlock()
|
||||
|
||||
if !ok {
|
||||
unresolvedRequests.Inc()
|
||||
http.NotFound(w, r) // TODO(Xe): brand this
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Set("X-Http-Ja4h-Fingerprint", ja4hFP)
|
||||
|
||||
if fp := fingerprint.GetTLSFingerprint(r); fp != nil {
|
||||
if ja3n := fp.JA3N(); ja3n != nil {
|
||||
r.Header.Set("X-Tls-Ja3n-Fingerprint", ja3n.String())
|
||||
}
|
||||
if ja4 := fp.JA4(); ja4 != nil {
|
||||
r.Header.Set("X-Tls-Ja4-Fingerprint", ja4.String())
|
||||
}
|
||||
}
|
||||
|
||||
if tcpFP := fingerprint.GetTCPFingerprint(r); tcpFP != nil {
|
||||
r.Header.Set("X-Tcp-Ja4t-Fingerprint", tcpFP.String())
|
||||
}
|
||||
|
||||
m := httpsnoop.CaptureMetrics(h, w, r)
|
||||
|
||||
requestsPerDomain.WithLabelValues(host, r.Method, fmt.Sprint(m.Code)).Inc()
|
||||
responseTime.WithLabelValues(host).Observe(float64(m.Duration.Milliseconds()))
|
||||
|
||||
slog.Info("request completed", "host", host, "method", r.Method, "response_code", m.Code, "duration_ms", m.Duration.Milliseconds())
|
||||
}
|
||||
319
cmd/osiris/internal/entrypoint/router_test.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
|
||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||
)
|
||||
|
||||
func loadConfig(t *testing.T, fname string) config.Toplevel {
|
||||
t.Helper()
|
||||
|
||||
var cfg config.Toplevel
|
||||
if err := hclsimple.DecodeFile(fname, nil, &cfg); err != nil {
|
||||
t.Fatalf("can't read configuration file %s: %v", fname, err)
|
||||
}
|
||||
|
||||
if err := cfg.Valid(); err != nil {
|
||||
t.Errorf("configuration file %s is invalid: %v", "./testdata/selfsigned.hcl", err)
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func newRouter(t *testing.T, cfg config.Toplevel) *Router {
|
||||
t.Helper()
|
||||
|
||||
rtr, err := NewRouter(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return rtr
|
||||
}
|
||||
|
||||
func TestNewRouter(t *testing.T) {
|
||||
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
|
||||
rtr := newRouter(t, cfg)
|
||||
|
||||
srv := httptest.NewServer(rtr)
|
||||
defer srv.Close()
|
||||
}
|
||||
|
||||
func TestNewRouterFails(t *testing.T) {
|
||||
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
|
||||
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "test1.internal",
|
||||
TLS: config.TLS{
|
||||
Cert: "./testdata/tls/invalid.crt",
|
||||
Key: "./testdata/tls/invalid.key",
|
||||
},
|
||||
Target: cfg.Domains[0].Target,
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
|
||||
rtr, err := NewRouter(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("wanted an error but got none")
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(rtr)
|
||||
defer srv.Close()
|
||||
}
|
||||
|
||||
func TestRouterSetConfig(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
configFname string
|
||||
mutation func(cfg config.Toplevel) config.Toplevel
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
configFname: "./testdata/good/selfsigned.hcl",
|
||||
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||
return cfg
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all schemes",
|
||||
configFname: "./testdata/good/selfsigned.hcl",
|
||||
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "http.internal",
|
||||
TLS: cfg.Domains[0].TLS,
|
||||
Target: "http://[::1]:3000",
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "https.internal",
|
||||
TLS: cfg.Domains[0].TLS,
|
||||
Target: "https://[::1]:3000",
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "h2c.internal",
|
||||
TLS: cfg.Domains[0].TLS,
|
||||
Target: "h2c://[::1]:3000",
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "unix.internal",
|
||||
TLS: cfg.Domains[0].TLS,
|
||||
Target: "unix://foo.sock",
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
|
||||
return cfg
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid TLS",
|
||||
configFname: "./testdata/good/selfsigned.hcl",
|
||||
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "test1.internal",
|
||||
TLS: config.TLS{
|
||||
Cert: "./testdata/tls/invalid.crt",
|
||||
Key: "./testdata/tls/invalid.key",
|
||||
},
|
||||
Target: cfg.Domains[0].Target,
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
|
||||
return cfg
|
||||
},
|
||||
err: ErrInvalidTLSKeypair,
|
||||
},
|
||||
{
|
||||
name: "target is not a valid URL",
|
||||
configFname: "./testdata/good/selfsigned.hcl",
|
||||
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "test1.internal",
|
||||
TLS: cfg.Domains[0].TLS,
|
||||
Target: "http://[::1:443",
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
|
||||
return cfg
|
||||
},
|
||||
err: ErrTargetInvalid,
|
||||
},
|
||||
{
|
||||
name: "invalid target scheme",
|
||||
configFname: "./testdata/good/selfsigned.hcl",
|
||||
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||
Name: "test1.internal",
|
||||
TLS: cfg.Domains[0].TLS,
|
||||
Target: "foo://",
|
||||
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||
})
|
||||
|
||||
return cfg
|
||||
},
|
||||
err: ErrNoHandler,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := loadConfig(t, tt.configFname)
|
||||
rtr := newRouter(t, cfg)
|
||||
|
||||
cfg = tt.mutation(cfg)
|
||||
|
||||
if err := rtr.setConfig(cfg); !errors.Is(err, tt.err) {
|
||||
t.Logf("want: %v", tt.err)
|
||||
t.Logf("got: %v", err)
|
||||
t.Error("got wrong error from rtr.setConfig function")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type ackHandler struct {
|
||||
ack bool
|
||||
}
|
||||
|
||||
func (ah *ackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ah.ack = true
|
||||
fmt.Fprintln(w, "OK")
|
||||
}
|
||||
|
||||
func (ah *ackHandler) Reset() {
|
||||
ah.ack = false
|
||||
}
|
||||
|
||||
func newUnixServer(t *testing.T, h http.Handler) string {
|
||||
sockName := filepath.Join(t.TempDir(), "s")
|
||||
ln, err := net.Listen("unix", sockName)
|
||||
if err != nil {
|
||||
t.Fatalf("can't listen on %s: %v", sockName, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
ln.Close()
|
||||
os.Remove(sockName)
|
||||
})
|
||||
|
||||
go func(ctx context.Context) {
|
||||
srv := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
srv.Close()
|
||||
}()
|
||||
|
||||
srv.Serve(ln)
|
||||
}(t.Context())
|
||||
|
||||
return "unix://" + sockName
|
||||
}
|
||||
|
||||
func TestRouterGetCertificate(t *testing.T) {
|
||||
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
|
||||
rtr := newRouter(t, cfg)
|
||||
|
||||
for _, tt := range []struct {
|
||||
domainName string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
domainName: "osiris.local.cetacean.club",
|
||||
},
|
||||
{
|
||||
domainName: "whacky-fun.local",
|
||||
err: ErrNoCert,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.domainName, func(t *testing.T) {
|
||||
if _, err := rtr.GetCertificate(&tls.ClientHelloInfo{ServerName: tt.domainName}); !errors.Is(err, tt.err) {
|
||||
t.Logf("want: %v", tt.err)
|
||||
t.Logf("got: %v", err)
|
||||
t.Error("got wrong error from rtr.GetCertificate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterServeAllProtocols(t *testing.T) {
|
||||
cfg := loadConfig(t, "./testdata/good/all_protocols.hcl")
|
||||
|
||||
httpAckHandler := &ackHandler{}
|
||||
httpsAckHandler := &ackHandler{}
|
||||
h2cAckHandler := &ackHandler{}
|
||||
unixAckHandler := &ackHandler{}
|
||||
|
||||
httpSrv := httptest.NewServer(httpAckHandler)
|
||||
httpsSrv := httptest.NewTLSServer(httpsAckHandler)
|
||||
h2cSrv := newH2cServer(t, h2cAckHandler)
|
||||
unixPath := newUnixServer(t, unixAckHandler)
|
||||
|
||||
cfg.Domains[0].Target = httpSrv.URL
|
||||
cfg.Domains[1].Target = httpsSrv.URL
|
||||
cfg.Domains[2].Target = strings.ReplaceAll(h2cSrv.URL, "http:", "h2c:")
|
||||
cfg.Domains[3].Target = unixPath
|
||||
|
||||
// enc := json.NewEncoder(os.Stderr)
|
||||
// enc.SetIndent("", " ")
|
||||
// enc.Encode(cfg)
|
||||
|
||||
rtr := newRouter(t, cfg)
|
||||
|
||||
cli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("plain http", func(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
ln.Close()
|
||||
})
|
||||
|
||||
go rtr.HandleHTTP(t.Context(), ln)
|
||||
|
||||
serverURL := "http://" + ln.Addr().String()
|
||||
t.Log(serverURL)
|
||||
|
||||
for _, d := range cfg.Domains {
|
||||
t.Run(d.Name, func(t *testing.T) {
|
||||
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, serverURL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Host = d.Name
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("wrong status code %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
0
cmd/osiris/internal/entrypoint/testdata/bad/empty.hcl
vendored
Normal file
15
cmd/osiris/internal/entrypoint/testdata/bad/invalid.hcl
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
bind {
|
||||
http = ":65530"
|
||||
https = ":65531"
|
||||
metrics = ":65532"
|
||||
}
|
||||
|
||||
domain "osiris.local.cetacean.club" {
|
||||
tls {
|
||||
cert = "./testdata/invalid.crt"
|
||||
key = "./testdata/invalid.key"
|
||||
}
|
||||
|
||||
target = "http://localhost:3000"
|
||||
health_target = "http://localhost:9091/healthz"
|
||||
}
|
||||
46
cmd/osiris/internal/entrypoint/testdata/good/all_protocols.hcl
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
bind {
|
||||
http = ":65520"
|
||||
https = ":65521"
|
||||
metrics = ":65522"
|
||||
}
|
||||
|
||||
domain "http.internal" {
|
||||
tls {
|
||||
cert = "./testdata/selfsigned.crt"
|
||||
key = "./testdata/selfsigned.key"
|
||||
}
|
||||
|
||||
target = "http://localhost:65510" # XXX(Xe) this is overwritten
|
||||
health_target = "http://localhost:9091/healthz"
|
||||
}
|
||||
|
||||
domain "https.internal" {
|
||||
tls {
|
||||
cert = "./testdata/selfsigned.crt"
|
||||
key = "./testdata/selfsigned.key"
|
||||
}
|
||||
|
||||
target = "https://localhost:65511" # XXX(Xe) this is overwritten
|
||||
insecure_skip_verify = true
|
||||
health_target = "http://localhost:9091/healthz"
|
||||
}
|
||||
|
||||
domain "h2c.internal" {
|
||||
tls {
|
||||
cert = "./testdata/selfsigned.crt"
|
||||
key = "./testdata/selfsigned.key"
|
||||
}
|
||||
|
||||
target = "h2c://localhost:65511" # XXX(Xe) this is overwritten
|
||||
health_target = "http://localhost:9091/healthz"
|
||||
}
|
||||
|
||||
domain "unix.internal" {
|
||||
tls {
|
||||
cert = "./testdata/selfsigned.crt"
|
||||
key = "./testdata/selfsigned.key"
|
||||
}
|
||||
|
||||
target = "http://localhost:65511" # XXX(Xe) this is overwritten
|
||||
health_target = "http://localhost:9091/healthz"
|
||||
}
|
||||
15
cmd/osiris/internal/entrypoint/testdata/good/selfsigned.hcl
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
bind {
|
||||
http = ":65530"
|
||||
https = ":65531"
|
||||
metrics = ":65532"
|
||||
}
|
||||
|
||||
domain "osiris.local.cetacean.club" {
|
||||
tls {
|
||||
cert = "./testdata/selfsigned.crt"
|
||||
key = "./testdata/selfsigned.key"
|
||||
}
|
||||
|
||||
target = "http://localhost:3000"
|
||||
health_target = "http://localhost:9091/healthz"
|
||||
}
|
||||
11
cmd/osiris/internal/entrypoint/testdata/selfsigned.crt
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnzCCAVGgAwIBAgIUOLTjSYOjFk00IemtFTC4oEZs988wBQYDK2VwMEUxCzAJ
|
||||
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MjEyNDIzWhcNMjUwODE3MjEyNDIz
|
||||
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAPHphABS15+4VV6R1
|
||||
vYzBQYIycQmOmlbA8QcfwzuB2VajUzBRMB0GA1UdDgQWBBT2s+MQ4AR6cbK4V0+d
|
||||
XZnok1orhDAfBgNVHSMEGDAWgBT2s+MQ4AR6cbK4V0+dXZnok1orhDAPBgNVHRMB
|
||||
Af8EBTADAQH/MAUGAytlcANBAOdoJbRMnHmkEETzVtXP+jkAI9yQNRXujnglApGP
|
||||
8I5pvIYVgYCgoQrnb4haVWFldHM1T9H698n19e/egfFb+w4=
|
||||
-----END CERTIFICATE-----
|
||||
3
cmd/osiris/internal/entrypoint/testdata/selfsigned.key
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIBop42tiZ0yzhaKo9NAc0PlAyBsE8NAE0i9Z7s2lgZuR
|
||||
-----END PRIVATE KEY-----
|
||||
43
cmd/osiris/main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/cmd/osiris/internal/entrypoint"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/facebookgo/flagenv"
|
||||
)
|
||||
|
||||
var (
|
||||
configFname = flag.String("config", "./osiris.hcl", "Configuration file (HCL), see docs")
|
||||
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
||||
versionFlag = flag.Bool("version", false, "if true, show version information then quit")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flagenv.Parse()
|
||||
flag.Parse()
|
||||
|
||||
if *versionFlag {
|
||||
fmt.Println("Osiris", anubis.Version)
|
||||
return
|
||||
}
|
||||
|
||||
internal.InitSlog(*slogLevel)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
if err := entrypoint.Main(ctx, entrypoint.Options{
|
||||
ConfigFname: *configFname,
|
||||
}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
15
cmd/osiris/osiris.hcl
Normal file
@@ -0,0 +1,15 @@
|
||||
bind {
|
||||
http = ":3004"
|
||||
https = ":3005"
|
||||
metrics = ":9091"
|
||||
}
|
||||
|
||||
domain "osiris.local.cetacean.club" {
|
||||
tls {
|
||||
cert = "./internal/config/testdata/tls/selfsigned.crt"
|
||||
key = "./internal/config/testdata/tls/selfsigned.key"
|
||||
}
|
||||
|
||||
target = "http://localhost:3000"
|
||||
health_target = "http://localhost:9091/healthz"
|
||||
}
|
||||
@@ -74,6 +74,25 @@ bots:
|
||||
weight:
|
||||
adjust: 10
|
||||
|
||||
# ## System load based checks.
|
||||
# # If the system is under high load, add weight.
|
||||
# - name: high-load-average
|
||||
# action: WEIGH
|
||||
# expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
|
||||
# weight:
|
||||
# adjust: 20
|
||||
|
||||
## If your backend service is running on the same operating system as Anubis,
|
||||
## you can uncomment this rule to make the challenge easier when the system is
|
||||
## under low load.
|
||||
##
|
||||
## If it is not, remove weight.
|
||||
# - name: low-load-average
|
||||
# action: WEIGH
|
||||
# expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
|
||||
# weight:
|
||||
# adjust: -10
|
||||
|
||||
# Generic catchall rule
|
||||
- name: generic-browser
|
||||
user_agent_regex: >-
|
||||
|
||||
223
data/services/uptime-robot.yaml
Normal file
@@ -0,0 +1,223 @@
|
||||
- name: uptime-robot
|
||||
user_agent_regex: UptimeRobot
|
||||
action: ALLOW
|
||||
# https://api.uptimerobot.com/meta/ips
|
||||
remote_addresses: [
|
||||
"3.12.251.153/32",
|
||||
"3.20.63.178/32",
|
||||
"3.77.67.4/32",
|
||||
"3.79.134.69/32",
|
||||
"3.105.133.239/32",
|
||||
"3.105.190.221/32",
|
||||
"3.133.226.214/32",
|
||||
"3.149.57.90/32",
|
||||
"3.212.128.62/32",
|
||||
"5.161.61.238/32",
|
||||
"5.161.73.160/32",
|
||||
"5.161.75.7/32",
|
||||
"5.161.113.195/32",
|
||||
"5.161.117.52/32",
|
||||
"5.161.177.47/32",
|
||||
"5.161.194.92/32",
|
||||
"5.161.215.244/32",
|
||||
"5.223.43.32/32",
|
||||
"5.223.53.147/32",
|
||||
"5.223.57.22/32",
|
||||
"18.116.205.62/32",
|
||||
"18.180.208.214/32",
|
||||
"18.192.166.72/32",
|
||||
"18.193.252.127/32",
|
||||
"24.144.78.39/32",
|
||||
"24.144.78.185/32",
|
||||
"34.198.201.66/32",
|
||||
"45.55.123.175/32",
|
||||
"45.55.127.146/32",
|
||||
"49.13.24.81/32",
|
||||
"49.13.130.29/32",
|
||||
"49.13.134.145/32",
|
||||
"49.13.164.148/32",
|
||||
"49.13.167.123/32",
|
||||
"52.15.147.27/32",
|
||||
"52.22.236.30/32",
|
||||
"52.28.162.93/32",
|
||||
"52.59.43.236/32",
|
||||
"52.87.72.16/32",
|
||||
"54.64.67.106/32",
|
||||
"54.79.28.129/32",
|
||||
"54.87.112.51/32",
|
||||
"54.167.223.174/32",
|
||||
"54.249.170.27/32",
|
||||
"63.178.84.147/32",
|
||||
"64.225.81.248/32",
|
||||
"64.225.82.147/32",
|
||||
"69.162.124.227/32",
|
||||
"69.162.124.235/32",
|
||||
"69.162.124.238/32",
|
||||
"78.46.190.63/32",
|
||||
"78.46.215.1/32",
|
||||
"78.47.98.55/32",
|
||||
"78.47.173.76/32",
|
||||
"88.99.80.227/32",
|
||||
"91.99.101.207/32",
|
||||
"128.140.41.193/32",
|
||||
"128.140.106.114/32",
|
||||
"129.212.132.140/32",
|
||||
"134.199.240.137/32",
|
||||
"138.197.53.117/32",
|
||||
"138.197.53.138/32",
|
||||
"138.197.54.143/32",
|
||||
"138.197.54.247/32",
|
||||
"138.197.63.92/32",
|
||||
"139.59.50.44/32",
|
||||
"142.132.180.39/32",
|
||||
"143.198.249.237/32",
|
||||
"143.198.250.89/32",
|
||||
"143.244.196.21/32",
|
||||
"143.244.196.211/32",
|
||||
"143.244.221.177/32",
|
||||
"144.126.251.21/32",
|
||||
"146.190.9.187/32",
|
||||
"152.42.149.135/32",
|
||||
"157.90.155.240/32",
|
||||
"157.90.156.63/32",
|
||||
"159.69.158.189/32",
|
||||
"159.223.243.219/32",
|
||||
"161.35.247.201/32",
|
||||
"167.99.18.52/32",
|
||||
"167.235.143.113/32",
|
||||
"168.119.53.160/32",
|
||||
"168.119.96.239/32",
|
||||
"168.119.123.75/32",
|
||||
"170.64.250.64/32",
|
||||
"170.64.250.132/32",
|
||||
"170.64.250.235/32",
|
||||
"178.156.181.172/32",
|
||||
"178.156.184.20/32",
|
||||
"178.156.185.127/32",
|
||||
"178.156.185.231/32",
|
||||
"178.156.187.238/32",
|
||||
"178.156.189.113/32",
|
||||
"178.156.189.249/32",
|
||||
"188.166.201.79/32",
|
||||
"206.189.241.133/32",
|
||||
"209.38.49.1/32",
|
||||
"209.38.49.206/32",
|
||||
"209.38.49.226/32",
|
||||
"209.38.51.43/32",
|
||||
"209.38.53.7/32",
|
||||
"209.38.124.252/32",
|
||||
"216.144.248.18/31",
|
||||
"216.144.248.21/32",
|
||||
"216.144.248.22/31",
|
||||
"216.144.248.24/30",
|
||||
"216.144.248.28/31",
|
||||
"216.144.248.30/32",
|
||||
"216.245.221.83/32",
|
||||
"2400:6180:10:200::56a0:b000/128",
|
||||
"2400:6180:10:200::56a0:c000/128",
|
||||
"2400:6180:10:200::56a0:e000/128",
|
||||
"2400:6180:100:d0::94b6:4001/128",
|
||||
"2400:6180:100:d0::94b6:5001/128",
|
||||
"2400:6180:100:d0::94b6:7001/128",
|
||||
"2406:da14:94d:8601:9d0d:7754:bedf:e4f5/128",
|
||||
"2406:da14:94d:8601:b325:ff58:2bba:7934/128",
|
||||
"2406:da14:94d:8601:db4b:c5ac:2cbe:9a79/128",
|
||||
"2406:da1c:9c8:dc02:7ae1:f2ea:ab91:2fde/128",
|
||||
"2406:da1c:9c8:dc02:7db9:f38b:7b9f:402e/128",
|
||||
"2406:da1c:9c8:dc02:82b2:f0fd:ee96:579/128",
|
||||
"2600:1f16:775:3a00:ac3:c5eb:7081:942e/128",
|
||||
"2600:1f16:775:3a00:37bf:6026:e54a:f03a/128",
|
||||
"2600:1f16:775:3a00:3f24:5bb0:95d7:5a6b/128",
|
||||
"2600:1f16:775:3a00:8c2c:2ba6:778f:5be5/128",
|
||||
"2600:1f16:775:3a00:91ac:3120:ff38:92b5/128",
|
||||
"2600:1f16:775:3a00:dbbe:36b0:3c45:da32/128",
|
||||
"2600:1f18:179:f900:71:af9a:ade7:d772/128",
|
||||
"2600:1f18:179:f900:2406:9399:4ae6:c5d3/128",
|
||||
"2600:1f18:179:f900:4696:7729:7bb3:f52f/128",
|
||||
"2600:1f18:179:f900:4b7d:d1cc:2d10:211/128",
|
||||
"2600:1f18:179:f900:5c68:91b6:5d75:5d7/128",
|
||||
"2600:1f18:179:f900:e8dd:eed1:a6c:183b/128",
|
||||
"2604:a880:800:14:0:1:68ba:d000/128",
|
||||
"2604:a880:800:14:0:1:68ba:e000/128",
|
||||
"2604:a880:800:14:0:1:68bb:0/128",
|
||||
"2604:a880:800:14:0:1:68bb:1000/128",
|
||||
"2604:a880:800:14:0:1:68bb:3000/128",
|
||||
"2604:a880:800:14:0:1:68bb:4000/128",
|
||||
"2604:a880:800:14:0:1:68bb:5000/128",
|
||||
"2604:a880:800:14:0:1:68bb:6000/128",
|
||||
"2604:a880:800:14:0:1:68bb:7000/128",
|
||||
"2604:a880:800:14:0:1:68bb:a000/128",
|
||||
"2604:a880:800:14:0:1:68bb:b000/128",
|
||||
"2604:a880:800:14:0:1:68bb:c000/128",
|
||||
"2604:a880:800:14:0:1:68bb:d000/128",
|
||||
"2604:a880:800:14:0:1:68bb:e000/128",
|
||||
"2604:a880:800:14:0:1:68bb:f000/128",
|
||||
"2607:ff68:107::4/128",
|
||||
"2607:ff68:107::14/128",
|
||||
"2607:ff68:107::33/128",
|
||||
"2607:ff68:107::48/127",
|
||||
"2607:ff68:107::50/125",
|
||||
"2607:ff68:107::58/127",
|
||||
"2607:ff68:107::60/128",
|
||||
"2a01:4f8:c0c:83fa::1/128",
|
||||
"2a01:4f8:c17:42e4::1/128",
|
||||
"2a01:4f8:c2c:9fc6::1/128",
|
||||
"2a01:4f8:c2c:beae::1/128",
|
||||
"2a01:4f8:1c1a:3d53::1/128",
|
||||
"2a01:4f8:1c1b:4ef4::1/128",
|
||||
"2a01:4f8:1c1b:5b5a::1/128",
|
||||
"2a01:4f8:1c1b:7ecc::1/128",
|
||||
"2a01:4f8:1c1c:11aa::1/128",
|
||||
"2a01:4f8:1c1c:5353::1/128",
|
||||
"2a01:4f8:1c1c:7240::1/128",
|
||||
"2a01:4f8:1c1c:a98a::1/128",
|
||||
"2a01:4f8:c012:c60e::1/128",
|
||||
"2a01:4f8:c013:c18::1/128",
|
||||
"2a01:4f8:c013:34c0::1/128",
|
||||
"2a01:4f8:c013:3b0f::1/128",
|
||||
"2a01:4f8:c013:3c52::1/128",
|
||||
"2a01:4f8:c013:3c53::1/128",
|
||||
"2a01:4f8:c013:3c54::1/128",
|
||||
"2a01:4f8:c013:3c55::1/128",
|
||||
"2a01:4f8:c013:3c56::1/128",
|
||||
"2a01:4ff:f0:bfd::1/128",
|
||||
"2a01:4ff:f0:2219::1/128",
|
||||
"2a01:4ff:f0:3e03::1/128",
|
||||
"2a01:4ff:f0:5f80::1/128",
|
||||
"2a01:4ff:f0:7fad::1/128",
|
||||
"2a01:4ff:f0:9c5f::1/128",
|
||||
"2a01:4ff:f0:b2f2::1/128",
|
||||
"2a01:4ff:f0:b6f1::1/128",
|
||||
"2a01:4ff:f0:d283::1/128",
|
||||
"2a01:4ff:f0:d3cd::1/128",
|
||||
"2a01:4ff:f0:e516::1/128",
|
||||
"2a01:4ff:f0:e9cf::1/128",
|
||||
"2a01:4ff:f0:eccb::1/128",
|
||||
"2a01:4ff:f0:efd1::1/128",
|
||||
"2a01:4ff:f0:fdc7::1/128",
|
||||
"2a01:4ff:2f0:193c::1/128",
|
||||
"2a01:4ff:2f0:27de::1/128",
|
||||
"2a01:4ff:2f0:3b3a::1/128",
|
||||
"2a03:b0c0:2:f0::bd91:f001/128",
|
||||
"2a03:b0c0:2:f0::bd92:1/128",
|
||||
"2a03:b0c0:2:f0::bd92:1001/128",
|
||||
"2a03:b0c0:2:f0::bd92:2001/128",
|
||||
"2a03:b0c0:2:f0::bd92:4001/128",
|
||||
"2a03:b0c0:2:f0::bd92:5001/128",
|
||||
"2a03:b0c0:2:f0::bd92:6001/128",
|
||||
"2a03:b0c0:2:f0::bd92:7001/128",
|
||||
"2a03:b0c0:2:f0::bd92:8001/128",
|
||||
"2a03:b0c0:2:f0::bd92:9001/128",
|
||||
"2a03:b0c0:2:f0::bd92:a001/128",
|
||||
"2a03:b0c0:2:f0::bd92:b001/128",
|
||||
"2a03:b0c0:2:f0::bd92:c001/128",
|
||||
"2a03:b0c0:2:f0::bd92:e001/128",
|
||||
"2a03:b0c0:2:f0::bd92:f001/128",
|
||||
"2a05:d014:1815:3400:6d:9235:c1c0:96ad/128",
|
||||
"2a05:d014:1815:3400:654f:bd37:724c:212b/128",
|
||||
"2a05:d014:1815:3400:90b4:4ef9:5631:b170/128",
|
||||
"2a05:d014:1815:3400:9779:d8e9:100a:9642/128",
|
||||
"2a05:d014:1815:3400:af29:e95e:64ff:df81/128",
|
||||
"2a05:d014:1815:3400:c7d6:f7f3:6cc1:30d1/128",
|
||||
"2a05:d014:1815:3400:d784:e5dd:8e0:67cb/128",
|
||||
]
|
||||
32
docker-bake.hcl
Normal file
@@ -0,0 +1,32 @@
|
||||
variable "ALPINE_VERSION" { default = "3.22" }
|
||||
variable "GITHUB_SHA" { default = "devel" }
|
||||
variable "VERSION" { default = "devel-docker" }
|
||||
|
||||
group "default" {
|
||||
targets = [
|
||||
"osiris",
|
||||
]
|
||||
}
|
||||
|
||||
target "osiris" {
|
||||
args = {
|
||||
ALPINE_VERSION = "3.22"
|
||||
VERSION = "${VERSION}"
|
||||
}
|
||||
context = "."
|
||||
dockerfile = "./docker/osiris.Dockerfile"
|
||||
platforms = [
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
"linux/arm/v7",
|
||||
"linux/ppc64le",
|
||||
"linux/riscv64",
|
||||
]
|
||||
pull = true
|
||||
sbom = true
|
||||
provenance = true
|
||||
tags = [
|
||||
"ghcr.io/techarohq/anubis/osiris:${VERSION}",
|
||||
"ghcr.io/techarohq/anubis/osiris:main"
|
||||
]
|
||||
}
|
||||
30
docker/osiris.Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
ARG ALPINE_VERSION=edge
|
||||
FROM --platform=${BUILDPLATFORM} alpine:${ALPINE_VERSION} AS build
|
||||
|
||||
RUN apk -U add go nodejs git build-base git npm bash zstd brotli gzip
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/root/go go mod download
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG VERSION=devel-docker
|
||||
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/root/go GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 GOARM=7 go build -gcflags "all=-N -l" -o /app/bin/osiris -ldflags "-s -w -extldflags -static -X github.com/TecharoHQ/anubis.Version=${VERSION}" ./cmd/osiris
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS run
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk -U add ca-certificates mailcap
|
||||
|
||||
COPY --from=build /app/bin/osiris /app/bin/osiris
|
||||
|
||||
CMD ["/app/bin/osiris"]
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
||||
@@ -19,5 +19,3 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Kubernetes manifests
|
||||
/manifest
|
||||
@@ -5,6 +5,7 @@ COPY . .
|
||||
|
||||
RUN npm ci && npm run build
|
||||
|
||||
FROM docker.io/library/nginx:alpine
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
FROM ghcr.io/xe/nginx-micro
|
||||
COPY --from=build /app/build /www
|
||||
COPY ./manifest/cfg/nginx/nginx.conf /conf
|
||||
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
||||
105
docs/blog/2025-07-09-incident-report/index.mdx
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
slug: incident/TI-20250709-0001
|
||||
title: "TI-20250709-0001: IPv4 traffic failures for Techaro services"
|
||||
authors: [xe]
|
||||
tags: [incident]
|
||||
image: ./window-portal.jpg
|
||||
---
|
||||
|
||||

|
||||
|
||||
Techaro services were down for IPv4 traffic on July 9th, 2025. This blogpost is a report of what happened, what actions were taken to resolve the situation, and what actions are being done in the near future to prevent this problem. Enjoy this incident report!
|
||||
|
||||
{/* truncate */}
|
||||
|
||||
:::note
|
||||
|
||||
In other companies, this kind of documentation would be kept internal. At Techaro, we believe that you deserve radical candor and the truth. As such, we are proving our lofty words with actions by publishing details about how things go wrong publicly.
|
||||
|
||||
Everything past this point follows my standard incident root cause meeting template.
|
||||
|
||||
:::
|
||||
|
||||
This incident report will focus on the services affected, timeline of what happened at which stage of the incident, where we got lucky, the root cause analysis, and what action items are being planned or taken to prevent this from happening in the future.
|
||||
|
||||
## Timeline
|
||||
|
||||
All events take place on July 9th, 2025.
|
||||
|
||||
| Time (UTC) | Description |
|
||||
| :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 12:32 | Uptime Kuma reports that another unrelated website on the same cluster was timing out. |
|
||||
| 12:33 | Uptime Kuma reports that Thoth's production endpoint is failing gRPC health checks. |
|
||||
| 12:35 | Investigation begins, [announcement made on Xe's Bluesky](https://bsky.app/profile/xeiaso.net/post/3ltjtdczpwc2x) due to the impact including their personal blog. |
|
||||
| 12:39 | `nginx-ingress` logs on the production cluster show IPv6 traffic but an abrupt cutoff in IPv4 traffic around 12:32 UTC. Ticket is opened with the hosting provider. |
|
||||
| 12:41 | IPv4 traffic resumes long enough for Uptime Kuma to report uptime, but then immediately fails again. |
|
||||
| 12:46 | IPv4 traffic resumes long enough for Uptime Kuma to report uptime, but then immediately fails again. (repeat instances of this have been scrubbed, but it happened about every 5-10 minutes) |
|
||||
| 12:48 | First reply from the hosting provider. |
|
||||
| 12:57 | Reply to hosting provider, ask to reboot the load balancer. |
|
||||
| 13:00 | Incident responder because busy due to a meeting under the belief that the downtime was out of their control and that uptime monitoring software would let them know if it came back up. |
|
||||
| 13:20 | Incident responder ended meeting and went back to monitoring downtime and preparing this document. |
|
||||
| 13:34 | IPv4 traffic starts to show up in the `ingress-nginx` logs. |
|
||||
| 13:35 | All services start to report healthy. Incident status changes to monitoring. |
|
||||
| 13:48 | Incident closed. |
|
||||
| 14:07 | Incident re-opened. Issues seem to be manifesting as BGP issues in the upstream provider. |
|
||||
| 14:10 | IPv4 traffic resumes and then stops. |
|
||||
| 14:18 | IPv4 traffic resumes again. Incident status changes to monitoring. |
|
||||
| 14:40 | Incident closed. |
|
||||
|
||||
## Services affected
|
||||
|
||||
| Service name | User impact |
|
||||
| :-------------------------------------------------- | :----------------- |
|
||||
| [Anubis Docs](https://anubis.techaro.lol) (IPv4) | Connection timeout |
|
||||
| [Anubis Docs](https://anubis.techaro.lol) (IPv6) | None |
|
||||
| [Thoth](/docs/admin/thoth/) (IPv4) | Connection timeout |
|
||||
| [Thoth](/docs/admin/thoth/) (IPv6) | None |
|
||||
| Other websites colocated on the same cluster (IPv4) | Connection timeout |
|
||||
| Other websites colocated on the same cluster (IPv6) | None |
|
||||
|
||||
## Root cause analysis
|
||||
|
||||
In simplify server management, Techaro runs a [Kubernetes](https://kubernetes.io/) cluster on [Vultr VKE](https://www.vultr.com/kubernetes/) (Vultr Kubernetes Engine). When you do this, Vultr needs to provision a [load balancer](https://docs.vultr.com/how-to-use-a-vultr-load-balancer-with-vke) to bridge the gap between the outside world and the Kubernetes world, kinda like this:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Overall architecture
|
||||
---
|
||||
|
||||
flowchart LR
|
||||
UT(User Traffic)
|
||||
subgraph Provider Infrastructure
|
||||
LB[Load Balancer]
|
||||
end
|
||||
subgraph Kubernetes
|
||||
IN(ingress-nginx)
|
||||
TH(Thoth)
|
||||
AN(Anubis Docs)
|
||||
OS(Other sites)
|
||||
|
||||
IN --> TH
|
||||
IN --> AN
|
||||
IN --> OS
|
||||
end
|
||||
|
||||
UT --> LB --> IN
|
||||
```
|
||||
|
||||
Techaro controls everything inside the Kubernetes side of that diagram. Anything else is out of our control. That load balancer is routed to the public internet via [Border Gateway Protocol (BGP)](https://en.wikipedia.org/wiki/Border_Gateway_Protocol).
|
||||
|
||||
If there is an interruption with the BGP sessions in the upstream provider, this can manifest as things either not working or inconsistently working. This is made more difficult by the fact that the IPv4 and IPv6 internets are technically separate networks. With this in mind, it's very possible to have IPv4 traffic fail but not IPv6 traffic.
|
||||
|
||||
The root cause is that the hosting provider we use for production services had flapping IPv4 BGP sessions in its Toronto region. When this happens all we can do is open a ticket and wait for it to come back up.
|
||||
|
||||
## Where we got lucky
|
||||
|
||||
The Uptime Kuma instance that caught this incident runs on an IPv4-only network. If it was dual stack, this would not have been caught as quickly.
|
||||
|
||||
The `ingress-nginx` logs print IP addresses of remote clients to the log feed. If this was not the case, it would be much more difficult to find this error.
|
||||
|
||||
## Action items
|
||||
|
||||
- A single instance of downtime like this is not enough reason to move providers. Moving providers because of this is thus out of scope.
|
||||
- Techaro needs a status page hosted on a different cloud provider than is used for the production cluster (`TecharoHQ/TODO#6`).
|
||||
- Health checks for IPv4 and IPv6 traffic need to be created (`TecharoHQ/TODO#7`).
|
||||
- Remove the requirement for [Anubis to pass Thoth health checks before it can start if Thoth is enabled](https://github.com/TecharoHQ/anubis/pull/794).
|
||||
BIN
docs/blog/2025-07-09-incident-report/window-portal.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
@@ -13,29 +13,129 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
<!-- This changes the project to: -->
|
||||
|
||||
- Expired records are now properly removed from bbolt databases ([#848](https://github.com/TecharoHQ/anubis/pull/848)).
|
||||
|
||||
- Fix hanging on service restart ([#853](https://github.com/TecharoHQ/anubis/issues/853))
|
||||
|
||||
### Added
|
||||
|
||||
Anubis now supports these new languages:
|
||||
|
||||
- [Czech](https://github.com/TecharoHQ/anubis/pull/849)
|
||||
|
||||
Anubis now supports the [`missingHeader`](./admin/configuration/expressions.mdx#missingHeader) to assert the absence of headers in requests.
|
||||
|
||||
## v1.21.0: Minfilia Warde
|
||||
|
||||
> Please, be at ease. You are among friends here.
|
||||
|
||||
In this release, Anubis becomes internationalized, gains the ability to use system load as input to issuing challenges, finally fixes the "invalid response" after "success" bug, and more! Please read these notes before upgrading as the changes are big enough that administrators should take action to ensure that the upgrade goes smoothly.
|
||||
|
||||
### Big ticket changes
|
||||
|
||||
The biggest change is that the ["invalid response" after "success" bug](https://github.com/TecharoHQ/anubis/issues/564) is now finally fixed for good by totally rewriting how Anubis' challenge issuance flow works. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. This also fixes [#746](https://github.com/TecharoHQ/anubis/issues/746) and other similar instances of this issue.
|
||||
|
||||
In order to reduce confusion, the "Success" interstitial that shows up when you pass a proof of work challenge has been removed.
|
||||
|
||||
#### Storage
|
||||
|
||||
Anubis now is able to store things persistently [in memory](./admin/policies.mdx#memory), [on the disk](./admin/policies.mdx#bbolt), or [in Valkey](./admin/policies.mdx#valkey) (this includes other compatible software). By default Anubis uses the in-memory backend. If you have an environment with mutable storage (even if it is temporary), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend.
|
||||
|
||||
#### Localization
|
||||
|
||||
Anubis now supports localized responses. Locales can be added in [lib/localization/locales/](https://github.com/TecharoHQ/anubis/tree/main/lib/localization/locales). This release includes support for the following languages:
|
||||
|
||||
- [Brazilian Portugese](https://github.com/TecharoHQ/anubis/pull/726)
|
||||
- [Chinese (Simplified)](https://github.com/TecharoHQ/anubis/pull/774)
|
||||
- [Chinese (Traditional)](https://github.com/TecharoHQ/anubis/pull/759)
|
||||
- English
|
||||
- [Estonian](https://github.com/TecharoHQ/anubis/pull/783)
|
||||
- [Filipino](https://github.com/TecharoHQ/anubis/pull/775)
|
||||
- [French](https://github.com/TecharoHQ/anubis/pull/716)
|
||||
- [German](https://github.com/TecharoHQ/anubis/pull/741)
|
||||
- [Icelandic](https://github.com/TecharoHQ/anubis/pull/780)
|
||||
- [Italian](https://github.com/TecharoHQ/anubis/pull/778)
|
||||
- [Japanese](https://github.com/TecharoHQ/anubis/pull/772)
|
||||
- [Spanish](https://github.com/TecharoHQ/anubis/pull/716)
|
||||
- [Turkish](https://github.com/TecharoHQ/anubis/pull/751)
|
||||
|
||||
If facts or local regulations demand, you can set Anubis default language with the `FORCED_LANGUAGE` environment variable or the `--forced-language` command line argument:
|
||||
|
||||
```sh
|
||||
FORCED_LANGUAGE=de
|
||||
```
|
||||
|
||||
#### Load average
|
||||
|
||||
Anubis can dynamically take action [based on the system load average](./admin/configuration/expressions.mdx#using-the-system-load-average), allowing you to write rules like this:
|
||||
|
||||
```yaml
|
||||
## System load based checks.
|
||||
# If the system is under high load for the last minute, add weight.
|
||||
- name: high-load-average
|
||||
action: WEIGH
|
||||
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: 20
|
||||
|
||||
# If it is not for the last 15 minutes, remove weight.
|
||||
- name: low-load-average
|
||||
action: WEIGH
|
||||
expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: -10
|
||||
```
|
||||
|
||||
Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example:
|
||||
|
||||
| Kind | Core count | Load threshold |
|
||||
| --------: | :--------- | :------------- |
|
||||
| high load | 4 | `8.0` |
|
||||
| low load | 4 | `2.0` |
|
||||
| high load | 16 | `32.0` |
|
||||
| low load | 16 | `8` |
|
||||
|
||||
Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
|
||||
|
||||
### Other features and fixes
|
||||
|
||||
There are a bunch of other assorted features and fixes too:
|
||||
|
||||
- Add `COOKIE_SECURE` option to set the cookie [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies)
|
||||
- Sets cookie defaults to use [SameSite: None](https://web.dev/articles/samesite-cookies-explained)
|
||||
- Determine the `BIND_NETWORK`/`--bind-network` value from the bind address ([#677](https://github.com/TecharoHQ/anubis/issues/677)).
|
||||
- Implement localization system. Find locale files in lib/localization/locales/.
|
||||
- Implement a [development container](https://containers.dev/) manifest to make contributions easier.
|
||||
- Fix dynamic cookie domains functionality ([#731](https://github.com/TecharoHQ/anubis/pull/731))
|
||||
- Add option for custom cookie prefix ([#732](https://github.com/TecharoHQ/anubis/pull/732))
|
||||
- Add translation for German language ([#741](https://github.com/TecharoHQ/anubis/pull/741))
|
||||
- Remove the "Success" interstitial after a proof of work challenge is concluded.
|
||||
- Anubis now has the concept of [storage backends](./admin/policies.mdx#storage-backends). These allow you to change how Anubis stores temporary data (in memory, on the disk, or in Valkey). If you run Anubis in an environment where you have a low amount of memory available for Anubis (eg: less than 64 megabytes), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend.
|
||||
- The challenge issuance and validation process has been rewritten from scratch. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. Fixes [#564](https://github.com/TecharoHQ/anubis/issues/564), [#746](https://github.com/TecharoHQ/anubis/issues/746), and other similar instances of this issue.
|
||||
- Make the [Open Graph](./admin/configuration/open-graph.mdx) subsystem and DNSBL subsystem use [storage backends](./admin/policies.mdx#storage-backends) instead of storing everything in memory by default.
|
||||
- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
|
||||
- Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751))
|
||||
- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
|
||||
- The [bbolt storage backend](./admin/policies.mdx#bbolt) now runs its cleanup every hour instead of every five minutes.
|
||||
- Don't block Anubis starting up if [Thoth](./admin/thoth.mdx) health checks fail.
|
||||
- A race condition involving [opening two challenge pages at once in different tabs](https://github.com/TecharoHQ/anubis/issues/832) causing one of them to fail has been fixed.
|
||||
- The "Try again" button on the error page has been fixed. Previously it meant "try the solution again" instead of "try the challenge again".
|
||||
- In certain cases, a user could be stuck with a test cookie that is invalid, locking them out of the service for up to half an hour. This has been fixed with better validation of this case and clearing the cookie.
|
||||
- Start exposing JA4H fingerprints for later use in CEL expressions.
|
||||
- Add `/healthz` route for use in platform-based health checks.
|
||||
|
||||
### Potentially breaking changes
|
||||
|
||||
We try to introduce breaking changes as much as possible, but these are the changes that may be relevant for you as an administrator:
|
||||
|
||||
#### Challenge format change
|
||||
|
||||
Previously Anubis did no accounting for challenges that it issued. This means that if Anubis restarted during a client, the client would be able to proceed once Anubis came back online.
|
||||
|
||||
During the upgrade to v1.21.0 and when v1.21.0 (or later) restarts with the [in-memory storage backend](./admin/policies.mdx#memory), you may see a higher rate of failed challenges than normal. If this persists beyond a few minutes, [open an issue](https://github.com/TecharoHQ/anubis/issues/new).
|
||||
|
||||
If you are using the in-memory storage backend, please consider using [a different storage backend](./admin/policies.mdx#storage-backends).
|
||||
|
||||
#### Systemd service changes
|
||||
|
||||
The following potentially breaking change applies to native installs with systemd only:
|
||||
|
||||
Each instance of systemd service template now has a unique `RuntimeDirectory`, as opposed to each instance of the service sharing a `RuntimeDirectory`. This change was made to avoid [the `RuntimeDirectory` getting nuked any time one of the Anubis instances restarts](https://github.com/TecharoHQ/anubis/issues/748).
|
||||
|
||||
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/sock` and additionally configure your HTTP load balancer as appropriate.
|
||||
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/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/):
|
||||
|
||||
@@ -45,6 +145,8 @@ If you need the legacy behaviour, install this [systemd unit dropin](https://www
|
||||
RuntimeDirectory=anubis
|
||||
```
|
||||
|
||||
Just keep in mind that this will cause problems when Anubis restarts.
|
||||
|
||||
## v1.20.0: Thancred Waters
|
||||
|
||||
The big ticket items are as follows:
|
||||
|
||||
@@ -77,7 +77,7 @@ For example, consider this rule:
|
||||
|
||||
For this rule, if a request comes in from `8.8.8.8` or `1.1.1.1`, Anubis will deny the request and return an error page.
|
||||
|
||||
#### `all` blocks
|
||||
### `all` blocks
|
||||
|
||||
An `all` block that contains a list of expressions. If all expressions in the list return `true`, then the action specified in the rule will be taken. If any of the expressions in the list returns `false`, Anubis will move on to the next rule.
|
||||
|
||||
@@ -99,15 +99,18 @@ For this rule, if a request comes in matching [the signature of the `go get` com
|
||||
|
||||
Anubis exposes the following variables to expressions:
|
||||
|
||||
| Name | Type | Explanation | Example |
|
||||
| :-------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
|
||||
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
|
||||
| `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. |
|
||||
| `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` |
|
||||
| `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` |
|
||||
| `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` |
|
||||
| `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` |
|
||||
| Name | Type | Explanation | Example |
|
||||
| :-------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
|
||||
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
|
||||
| `load_1m` | `double` | The current system load average over the last one minute. This is useful for making [load-based checks](#using-the-system-load-average). |
|
||||
| `load_5m` | `double` | The current system load average over the last five minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
|
||||
| `load_15m` | `double` | The current system load average over the last fifteen minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
|
||||
| `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. |
|
||||
| `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` |
|
||||
| `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` |
|
||||
| `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` |
|
||||
| `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` |
|
||||
|
||||
Of note: in many languages when you look up a key in a map and there is nothing there, the language will return some "falsy" value like `undefined` in JavaScript, `None` in Python, or the zero value of the type in Go. In CEL, if you try to look up a value that does not exist, execution of the expression will fail and Anubis will return an error.
|
||||
|
||||
@@ -120,7 +123,7 @@ In order to avoid this, make sure the header or query parameter you are testing
|
||||
- 'path == "/index.php"'
|
||||
- '"title" in query'
|
||||
- '"action" in query'
|
||||
- 'query["action"] == "history"
|
||||
- 'query["action"] == "history"'
|
||||
```
|
||||
|
||||
This rule throws a challenge if and only if all of the following conditions are true:
|
||||
@@ -141,12 +144,74 @@ X-Real-Ip: 8.8.8.8
|
||||
|
||||
Anubis would return a challenge because all of those conditions are true.
|
||||
|
||||
### Using the system load average
|
||||
|
||||
In Unix-like systems (such as Linux), every process on the system has to wait its turn to be able to run. This means that as more processes on the system are running, they need to wait longer to be able to execute. The [load average](<https://en.wikipedia.org/wiki/Load_(computing)>) represents the number of processes that want to be able to run but can't run yet. This metric isn't the most reliable to identify a cause, but is great at helping to identify symptoms.
|
||||
|
||||
Anubis lets you use the system load average as an input to expressions so that you can make dynamic rules like "when the system is under a low amount of load, dial back the protection, but when it's under a lot of load, crank it up to the mix". This lets you get all of the blocking features of Anubis in the background but only really expose Anubis to users when the system is actively being attacked.
|
||||
|
||||
This is best combined with the [weight](../policies.mdx#request-weight) and [threshold](./thresholds.mdx) systems so that you can have Anubis dynamically respond to attacks. Consider these rules in the default configuration file:
|
||||
|
||||
```yaml
|
||||
## System load based checks.
|
||||
# If the system is under high load for the last minute, add weight.
|
||||
- name: high-load-average
|
||||
action: WEIGH
|
||||
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: 20
|
||||
|
||||
# If it is not for the last 15 minutes, remove weight.
|
||||
- name: low-load-average
|
||||
action: WEIGH
|
||||
expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
|
||||
weight:
|
||||
adjust: -10
|
||||
```
|
||||
|
||||
This combination of rules makes Anubis dynamically react to the system load and only kick in when the system is under attack.
|
||||
|
||||
Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example:
|
||||
|
||||
| Kind | Core count | Load threshold |
|
||||
| --------: | :--------- | :------------- |
|
||||
| high load | 4 | `8.0` |
|
||||
| low load | 4 | `2.0` |
|
||||
| high load | 16 | `32.0` |
|
||||
| low load | 16 | `8` |
|
||||
|
||||
Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
|
||||
|
||||
## Functions exposed to Anubis expressions
|
||||
|
||||
Anubis expressions can be augmented with the following functions:
|
||||
|
||||
### `missingHeader`
|
||||
|
||||
Available in `bot` expressions.
|
||||
|
||||
```ts
|
||||
function missingHeader(headers: Record<string, string>, key: string) bool
|
||||
```
|
||||
|
||||
`missingHeader` returns `true` if the request does not contain a header. This is useful when you are trying to assert behavior such as:
|
||||
|
||||
```yaml
|
||||
# Adds weight to old versions of Chrome
|
||||
- name: old-chrome
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: 10
|
||||
expression:
|
||||
all:
|
||||
- userAgent.matches("Chrome/[1-9][0-9]?\\.0\\.0\\.0")
|
||||
- missingHeader(headers, "Sec-Ch-Ua")
|
||||
```
|
||||
|
||||
### `randInt`
|
||||
|
||||
Available in all expressions.
|
||||
|
||||
```ts
|
||||
function randInt(n: int): int;
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ Effectively you have one trip through Apache to do TLS termination, a detour thr
|
||||
|
||||
:::note
|
||||
|
||||
These examples assume that you are using a setup where your nginx configuration is made up of a bunch of files in `/etc/httpd/conf.d/*.conf`. This is not true for all deployments of Apache. If you are not in such an environment, append these snippets to your `/etc/httpd/conf/httpd.conf` file.
|
||||
These examples assume that you are using a setup where your Apache configuration is made up of a bunch of files in `/etc/httpd/conf.d/*.conf`. This is not true for all deployments of Apache. If you are not in such an environment, append these snippets to your `/etc/httpd/conf/httpd.conf` file.
|
||||
|
||||
:::
|
||||
|
||||
@@ -56,6 +56,7 @@ Assuming you are protecting `anubistest.techaro.lol`, you need the following ser
|
||||
</VirtualHost>
|
||||
|
||||
# HTTPS listener that forwards to Anubis
|
||||
<IfModule mod_proxy.c>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin your@email.here
|
||||
ServerName anubistest.techaro.lol
|
||||
|
||||
@@ -4,7 +4,7 @@ Docker compose is typically used in concert with other load balancers such as [A
|
||||
|
||||
```yaml
|
||||
services:
|
||||
anubis-nginx:
|
||||
anubis:
|
||||
image: ghcr.io/techarohq/anubis:latest
|
||||
environment:
|
||||
BIND: ":8080"
|
||||
@@ -15,10 +15,17 @@ services:
|
||||
POLICY_FNAME: "/data/cfg/botPolicy.yaml"
|
||||
OG_PASSTHROUGH: "true"
|
||||
OG_EXPIRY_TIME: "24h"
|
||||
healthcheck:
|
||||
test: ["CMD", "anubis", "--healthcheck"]
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 500ms
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- "./botPolicy.yaml:/data/cfg/botPolicy.yaml:ro"
|
||||
|
||||
nginx:
|
||||
image: nginx
|
||||
volumes:
|
||||
|
||||
@@ -69,7 +69,7 @@ Anubis uses these environment variables for configuration:
|
||||
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
||||
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
||||
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
|
||||
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
|
||||
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
||||
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
||||
|
||||
@@ -137,7 +137,7 @@ Test to make sure it's running with `curl`:
|
||||
curl http://localhost:8240/metrics
|
||||
```
|
||||
|
||||
Then set up your reverse proxy (Nginx, Caddy, etc.) to point to the Anubis port. Anubis will then reverse proxy all requests that meet the policies in `/etc/anubis/gitea.botPolicies.json` to the target service.
|
||||
Then set up your reverse proxy (Nginx, Caddy, etc.) to point to the Anubis port. Anubis will then reverse proxy all requests that meet the policies in `/etc/anubis/gitea.botPolicies.yaml` to the target service.
|
||||
|
||||
For more details on particular reverse proxies, see here:
|
||||
|
||||
|
||||
@@ -268,6 +268,12 @@ The memory backend is an in-memory cache. This backend works best if you don't u
|
||||
|
||||
The biggest downside is that there is not currently a limit to how much data can be stored in memory. This will be addressed at a later time.
|
||||
|
||||
:::warning
|
||||
|
||||
The in-memory backend exists mostly for validation, testing, and to ensure that the default configuration of Anubis works as expected. Do not use this persistently in production.
|
||||
|
||||
:::
|
||||
|
||||
#### Configuration
|
||||
|
||||
The memory backend does not require any configuration to use.
|
||||
@@ -289,10 +295,9 @@ When Anubis opens a bbolt database, it takes an exclusive lock on that database.
|
||||
|
||||
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. |
|
||||
| Name | Type | Example | Description |
|
||||
| :----- | :--- | :----------------- | :--------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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:
|
||||
|
||||
|
||||
@@ -58,6 +58,20 @@ Anubis is brought to you by sponsors and donors like:
|
||||
<a href="https://wildbase.xyz/">
|
||||
<img src="/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64" />
|
||||
</a>
|
||||
<a href="https://emma.pet">
|
||||
<img
|
||||
src="/img/sponsors/nepeat-logo.webp"
|
||||
alt="Cat eyes over the word Emma in a serif font"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://fabulous.systems/">
|
||||
<img
|
||||
src="/img/sponsors/fabulous-systems.webp"
|
||||
alt="Cat eyes over the word Emma in a serif font"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -50,6 +50,14 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
||||
- https://coinhoards.org/
|
||||
- https://pluralpedia.org/
|
||||
- https://git.aya.so/
|
||||
- https://marginalia-search.com/
|
||||
- https://repositorio.ufrn.br/home/
|
||||
- https://mozillazine.org/
|
||||
- https://clew.se/
|
||||
- https://tumfatig.net/
|
||||
- https://rpmfusion.org/
|
||||
- https://wiki.freepascal.org/
|
||||
- https://azurlane.koumakan.jp/
|
||||
- <details>
|
||||
<summary>FreeCAD</summary>
|
||||
- https://forum.freecad.org/
|
||||
@@ -94,3 +102,14 @@ This page contains a non-exhaustive list with all websites using Anubis.
|
||||
- https://find.library.duke.edu/
|
||||
- https://nicholas.duke.edu/
|
||||
</details>
|
||||
- <details>
|
||||
<summary>Forschungszentrum Jülich</summary>
|
||||
- https://juser.fz-juelich.de/
|
||||
</details>
|
||||
- <details>
|
||||
<summary>archlinux32.org</summary>
|
||||
- https://www.archlinux32.org/packages/
|
||||
- https://bbs.archlinux32.org/
|
||||
- https://bugs.archlinux32.org/
|
||||
</details>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type * as Preset from '@docusaurus/preset-classic';
|
||||
|
||||
const config: Config = {
|
||||
title: 'Anubis',
|
||||
tagline: 'Weigh the soul of incoming HTTP requests using proof-of-work to stop AI crawlers',
|
||||
tagline: 'Weigh the soul of incoming HTTP requests to protect your website!',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// Set the production url of your site here
|
||||
@@ -40,27 +40,20 @@ const config: Config = {
|
||||
[
|
||||
'classic',
|
||||
{
|
||||
docs: {
|
||||
sidebarPath: './sidebars.ts',
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/TecharoHQ/anubis/tree/main/docs/',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
feedOptions: {
|
||||
type: ['rss', 'atom', "json"],
|
||||
xslt: true,
|
||||
},
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
// Useful options to enforce blogging best practices
|
||||
editUrl: 'https://github.com/TecharoHQ/anubis/tree/main/docs/',
|
||||
onInlineTags: 'warn',
|
||||
onInlineAuthors: 'warn',
|
||||
onUntruncatedBlogPosts: 'warn',
|
||||
onUntruncatedBlogPosts: 'throw',
|
||||
},
|
||||
docs: {
|
||||
sidebarPath: './sidebars.ts',
|
||||
editUrl: 'https://github.com/TecharoHQ/anubis/tree/main/docs/',
|
||||
},
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
@@ -74,7 +67,7 @@ const config: Config = {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
// Replace with your project's social card
|
||||
image: 'img/docusaurus-social-card.jpg',
|
||||
image: 'img/social-card.jpg',
|
||||
navbar: {
|
||||
title: 'Anubis',
|
||||
logo: {
|
||||
@@ -82,23 +75,28 @@ const config: Config = {
|
||||
src: 'img/favicon.webp',
|
||||
},
|
||||
items: [
|
||||
{ to: '/blog', label: 'Blog', position: 'left' },
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'tutorialSidebar',
|
||||
position: 'left',
|
||||
label: 'Docs',
|
||||
},
|
||||
{ to: '/blog', label: 'Blog', position: 'left' },
|
||||
{
|
||||
href: 'https://github.com/sponsors/Xe',
|
||||
label: "Sponsorship",
|
||||
position: 'left'
|
||||
to: '/docs/admin/botstopper',
|
||||
label: "Unbranded Version",
|
||||
position: "left"
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/TecharoHQ/anubis',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/sponsors/Xe',
|
||||
label: "Sponsor the Project",
|
||||
position: 'right'
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
@@ -141,6 +139,10 @@ const config: Config = {
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/TecharoHQ/anubis',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
href: 'https://techarohq.github.io/status/'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
19
docs/fly.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
app = 'anubis-docs'
|
||||
primary_region = 'yyz'
|
||||
|
||||
[build]
|
||||
image = "ghcr.io/techarohq/anubis/docs:main"
|
||||
|
||||
[http_service]
|
||||
internal_port = 80
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ['app']
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
memory_mb = 256
|
||||
|
||||
99
docs/manifest/cfg/nginx/mime.types
Normal file
@@ -0,0 +1,99 @@
|
||||
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/javascript js;
|
||||
application/atom+xml atom;
|
||||
application/rss+xml rss;
|
||||
|
||||
text/mathml mml;
|
||||
text/plain txt;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
text/vnd.wap.wml wml;
|
||||
text/x-component htc;
|
||||
|
||||
image/avif avif;
|
||||
image/png png;
|
||||
image/svg+xml svg svgz;
|
||||
image/tiff tif tiff;
|
||||
image/vnd.wap.wbmp wbmp;
|
||||
image/webp webp;
|
||||
image/x-icon ico;
|
||||
image/x-jng jng;
|
||||
image/x-ms-bmp bmp;
|
||||
|
||||
font/woff woff;
|
||||
font/woff2 woff2;
|
||||
|
||||
application/java-archive jar war ear;
|
||||
application/json json;
|
||||
application/mac-binhex40 hqx;
|
||||
application/msword doc;
|
||||
application/pdf pdf;
|
||||
application/postscript ps eps ai;
|
||||
application/rtf rtf;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
application/vnd.google-earth.kml+xml kml;
|
||||
application/vnd.google-earth.kmz kmz;
|
||||
application/vnd.ms-excel xls;
|
||||
application/vnd.ms-fontobject eot;
|
||||
application/vnd.ms-powerpoint ppt;
|
||||
application/vnd.oasis.opendocument.graphics odg;
|
||||
application/vnd.oasis.opendocument.presentation odp;
|
||||
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||
application/vnd.oasis.opendocument.text odt;
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
pptx;
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
xlsx;
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
docx;
|
||||
application/vnd.wap.wmlc wmlc;
|
||||
application/wasm wasm;
|
||||
application/x-7z-compressed 7z;
|
||||
application/x-cocoa cco;
|
||||
application/x-java-archive-diff jardiff;
|
||||
application/x-java-jnlp-file jnlp;
|
||||
application/x-makeself run;
|
||||
application/x-perl pl pm;
|
||||
application/x-pilot prc pdb;
|
||||
application/x-rar-compressed rar;
|
||||
application/x-redhat-package-manager rpm;
|
||||
application/x-sea sea;
|
||||
application/x-shockwave-flash swf;
|
||||
application/x-stuffit sit;
|
||||
application/x-tcl tcl tk;
|
||||
application/x-x509-ca-cert der pem crt;
|
||||
application/x-xpinstall xpi;
|
||||
application/xhtml+xml xhtml;
|
||||
application/xspf+xml xspf;
|
||||
application/zip zip;
|
||||
|
||||
application/octet-stream bin exe dll;
|
||||
application/octet-stream deb;
|
||||
application/octet-stream dmg;
|
||||
application/octet-stream iso img;
|
||||
application/octet-stream msi msp msm;
|
||||
|
||||
audio/midi mid midi kar;
|
||||
audio/mpeg mp3;
|
||||
audio/ogg ogg;
|
||||
audio/x-m4a m4a;
|
||||
audio/x-realaudio ra;
|
||||
|
||||
video/3gpp 3gpp 3gp;
|
||||
video/mp2t ts;
|
||||
video/mp4 mp4;
|
||||
video/mpeg mpeg mpg;
|
||||
video/quicktime mov;
|
||||
video/webm webm;
|
||||
video/x-flv flv;
|
||||
video/x-m4v m4v;
|
||||
video/x-mng mng;
|
||||
video/x-ms-asf asx asf;
|
||||
video/x-ms-wmv wmv;
|
||||
video/x-msvideo avi;
|
||||
}
|
||||
31
docs/manifest/cfg/nginx/nginx.conf
Normal file
@@ -0,0 +1,31 @@
|
||||
user nginx;
|
||||
worker_processes 2;
|
||||
error_log /dev/stdout warn;
|
||||
pid /nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /dev/stdout;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
root /www;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ spec:
|
||||
- name: anubis
|
||||
configMap:
|
||||
name: anubis-cfg
|
||||
- name: nginx
|
||||
configMap:
|
||||
name: nginx-cfg
|
||||
- name: temporary-data
|
||||
emptyDir: {}
|
||||
containers:
|
||||
@@ -28,8 +31,23 @@ spec:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
volumeMounts:
|
||||
- name: nginx
|
||||
mountPath: /conf
|
||||
ports:
|
||||
- containerPort: 80
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
- name: anubis
|
||||
image: ghcr.io/techarohq/anubis:main
|
||||
imagePullPolicy: Always
|
||||
@@ -75,3 +93,15 @@ spec:
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: anubis-docs-thoth
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 9090
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 9090
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
|
||||
@@ -11,3 +11,8 @@ configMapGenerator:
|
||||
behavior: create
|
||||
files:
|
||||
- ./cfg/anubis/botPolicies.yaml
|
||||
- name: nginx-cfg
|
||||
behavior: create
|
||||
files:
|
||||
- ./cfg/nginx/mime.types
|
||||
- ./cfg/nginx/nginx.conf
|
||||
|
||||
@@ -5,49 +5,50 @@ import styles from "./styles.module.css";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
Svg: React.ComponentType<React.ComponentProps<"svg">>;
|
||||
imageURL: string;
|
||||
description: ReactNode;
|
||||
};
|
||||
|
||||
const FeatureList: FeatureItem[] = [
|
||||
{
|
||||
title: "Easy to Use",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
|
||||
imageURL: require("@site/static/img/anubis/happy.webp").default,
|
||||
description: (
|
||||
<>
|
||||
Anubis is easy to set up, lightweight, and helps get rid of the lowest
|
||||
hanging fruit so you can sleep at night.
|
||||
Anubis sits in the background and weighs the risk of incoming requests.
|
||||
If it asks a client to complete a challenge, no user interaction is
|
||||
required.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Lightweight",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
|
||||
imageURL: require("@site/static/img/anubis/pensive.webp").default,
|
||||
description: (
|
||||
<>
|
||||
Anubis is efficient and as lightweight as possible, blocking the worst
|
||||
of the bots on the internet and makes it easy to protect what you host
|
||||
online.
|
||||
Anubis is so lightweight you'll forget it's there until you look at your
|
||||
hosting bill. On average it uses less than 128 MB of ram.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Multi-threaded",
|
||||
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
|
||||
title: "Block the scrapers",
|
||||
imageURL: require("@site/static/img/anubis/reject.webp").default,
|
||||
description: (
|
||||
<>
|
||||
Anubis uses a multi-threaded proof of work check to ensure that users
|
||||
browsers are up to date and support modern standards.
|
||||
Anubis uses a combination of heuristics to identify and block bots
|
||||
before they take your website down. You can customize the rules with{" "}
|
||||
<a href="/docs/admin/policies">your own policies</a>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
function Feature({ title, description, imageURL }: FeatureItem) {
|
||||
return (
|
||||
<div className={clsx("col col--4")}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
<img src={imageURL} className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<Heading as="h3">{title}</Heading>
|
||||
|
||||
@@ -31,19 +31,12 @@ export default function Home(): ReactNode {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`Anubis: self hostable scraper defense software`}
|
||||
description="Weigh the soul of incoming HTTP requests using proof-of-work to stop AI crawlers"
|
||||
title={`Anubis: Web AI Firewall Utility`}
|
||||
description="Weigh the soul of incoming HTTP requests to protect your website!"
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
|
||||
<center>
|
||||
<p>
|
||||
This is all placeholder text. It will be fixed. Give me time. I am
|
||||
one person and my project has unexpectedly gone viral.
|
||||
</p>
|
||||
</center>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
BIN
docs/static/img/anubis/happy.webp
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/static/img/anubis/pensive.webp
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/anubis/reject.webp
vendored
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/static/img/docusaurus-social-card.jpg
vendored
|
Before Width: | Height: | Size: 54 KiB |
BIN
docs/static/img/social-card.jpg
vendored
Normal file
|
After Width: | Height: | Size: 881 KiB |
BIN
docs/static/img/sponsors/fabulous-systems.webp
vendored
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
docs/static/img/sponsors/nepeat-logo.webp
vendored
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
61
go.mod
@@ -4,34 +4,37 @@ go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/TecharoHQ/thoth-proto v0.4.0
|
||||
github.com/a-h/templ v0.3.906
|
||||
github.com/a-h/templ v0.3.920
|
||||
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/gaissmai/bart v0.22.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/google/cel-go v0.26.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
github.com/playwright-community/playwright-go v0.5200.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||
github.com/shirou/gopsutil/v4 v4.25.6
|
||||
github.com/testcontainers/testcontainers-go v0.38.0
|
||||
go.etcd.io/bbolt v1.4.2
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/text v0.26.0
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/text v0.27.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.33.2
|
||||
k8s.io/apimachinery v0.33.3
|
||||
sigs.k8s.io/yaml v1.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 // indirect
|
||||
cel.dev/expr v0.23.1 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
@@ -44,8 +47,10 @@ require (
|
||||
github.com/Songmu/gitconfig v0.2.0 // indirect
|
||||
github.com/TecharoHQ/yeet v0.6.0 // indirect
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||
@@ -53,6 +58,8 @@ 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/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // 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
|
||||
@@ -63,11 +70,11 @@ require (
|
||||
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/docker v28.2.2+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/ebitengine/purego v0.8.4 // 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
|
||||
@@ -79,7 +86,7 @@ require (
|
||||
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/logr v1.4.3 // 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
|
||||
@@ -88,6 +95,7 @@ require (
|
||||
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-cmp v0.7.0 // 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
|
||||
@@ -96,6 +104,7 @@ require (
|
||||
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/hashicorp/hcl/v2 v2.24.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
|
||||
@@ -106,11 +115,13 @@ require (
|
||||
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/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // 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/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.4.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
|
||||
@@ -127,7 +138,6 @@ require (
|
||||
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
|
||||
@@ -138,7 +148,6 @@ require (
|
||||
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
|
||||
@@ -146,28 +155,32 @@ require (
|
||||
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
|
||||
github.com/zclconf/go-cty v1.16.3 // 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.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.3 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/crypto v0.40.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/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/term 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-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // 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
|
||||
|
||||
136
go.sum
@@ -2,10 +2,12 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeX
|
||||
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
|
||||
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
|
||||
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
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/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
@@ -38,14 +40,18 @@ 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.906 h1:ZUThc8Q9n04UATaCwaG60pB1AqbulLmYEAMnWV63svg=
|
||||
github.com/a-h/templ v0.3.906/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/a-h/templ v0.3.920 h1:IQjjTu4KGrYreHo/ewzSeS8uefecisPayIIc9VflLSE=
|
||||
github.com/a-h/templ v0.3.920/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=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -75,6 +81,10 @@ 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/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
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=
|
||||
@@ -99,16 +109,16 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
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/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
|
||||
github.com/docker/docker v28.2.2+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/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/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=
|
||||
@@ -131,8 +141,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gaissmai/bart v0.20.4 h1:Ik47r1fy3jRVU+1eYzKSW3ho2UgBVTVnUS8O993584U=
|
||||
github.com/gaissmai/bart v0.20.4/go.mod h1:cEed+ge8dalcbpi8wtS9x9m2hn/fNJH5suhdGQOHnYk=
|
||||
github.com/gaissmai/bart v0.22.0 h1:+yR2mCpZx8H8GlqA+Icqi7/Iwx2/OUbO4bVbsORK0ns=
|
||||
github.com/gaissmai/bart v0.22.0/go.mod h1:RpLtt3lWq1BoRz3AAyDAJ7jhLWBkYhVCfi+ximB2t68=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
@@ -146,8 +156,8 @@ github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj
|
||||
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/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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=
|
||||
@@ -173,14 +183,14 @@ 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-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
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=
|
||||
@@ -214,9 +224,13 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs
|
||||
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/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
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/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
|
||||
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
|
||||
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=
|
||||
@@ -249,6 +263,8 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be h1:dVIND0nXGXPQnFZYrMXT6CxHhBYhTPMm0GFqcmfaIC4=
|
||||
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be/go.mod h1:q68TUR45WDa2r3yU4aO6WgxfCc0Vj1qtRaKaRE3yMLM=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
@@ -268,16 +284,22 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
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/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
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/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
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/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
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=
|
||||
@@ -329,8 +351,8 @@ github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/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=
|
||||
@@ -351,6 +373,8 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -365,8 +389,8 @@ github.com/suzuki-shunsuke/pinact v1.6.0 h1:2QvSzREOquwLwKXhF9Hj0AInE/Rl63SZz9dK
|
||||
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/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
|
||||
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
|
||||
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=
|
||||
@@ -387,6 +411,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
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=
|
||||
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
|
||||
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
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=
|
||||
@@ -395,16 +421,22 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
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 v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
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.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
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=
|
||||
@@ -415,8 +447,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
|
||||
@@ -436,8 +468,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
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=
|
||||
@@ -445,8 +477,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.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=
|
||||
@@ -473,8 +505,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
|
||||
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -482,8 +514,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -492,8 +524,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
@@ -511,10 +545,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
||||
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-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/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/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=
|
||||
@@ -533,10 +567,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
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.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
|
||||
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
|
||||
k8s.io/apimachinery v0.33.3/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=
|
||||
|
||||
97
internal/fingerprint/ja3n.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// TLSFingerprintJA3N represents a JA3N fingerprint
|
||||
type TLSFingerprintJA3N [md5.Size]byte
|
||||
|
||||
func (f TLSFingerprintJA3N) String() string {
|
||||
return hex.EncodeToString(f[:])
|
||||
}
|
||||
|
||||
func buildJA3N(hello *tls.ClientHelloInfo, sortExtensions bool) TLSFingerprintJA3N {
|
||||
buf := make([]byte, 0, 256)
|
||||
|
||||
{
|
||||
var sslVersion uint16
|
||||
var hasGrease bool
|
||||
for _, v := range hello.SupportedVersions {
|
||||
if v&greaseMask != greaseValue {
|
||||
if v > sslVersion {
|
||||
sslVersion = v
|
||||
}
|
||||
} else {
|
||||
hasGrease = true
|
||||
}
|
||||
}
|
||||
|
||||
// maximum TLS 1.2 as specified on JA3, as TLS 1.3 is put in SupportedVersions
|
||||
if slices.Contains(hello.Extensions, extensionSupportedVersions) && hasGrease && sslVersion > tls.VersionTLS12 {
|
||||
sslVersion = tls.VersionTLS12
|
||||
}
|
||||
|
||||
buf = strconv.AppendUint(buf, uint64(sslVersion), 10)
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
|
||||
n := 0
|
||||
for _, cipher := range hello.CipherSuites {
|
||||
//if !slices.Contains(greaseValues[:], cipher) {
|
||||
if cipher&greaseMask != greaseValue {
|
||||
buf = strconv.AppendUint(buf, uint64(cipher), 10)
|
||||
buf = append(buf, '-')
|
||||
n = 1
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:len(buf)-n]
|
||||
buf = append(buf, ',')
|
||||
n = 0
|
||||
|
||||
extensions := hello.Extensions
|
||||
if sortExtensions {
|
||||
extensions = slices.Clone(extensions)
|
||||
slices.Sort(extensions)
|
||||
}
|
||||
|
||||
for _, extension := range extensions {
|
||||
if extension&greaseMask != greaseValue {
|
||||
buf = strconv.AppendUint(buf, uint64(extension), 10)
|
||||
buf = append(buf, '-')
|
||||
n = 1
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:len(buf)-n]
|
||||
buf = append(buf, ',')
|
||||
n = 0
|
||||
|
||||
for _, curve := range hello.SupportedCurves {
|
||||
if curve&greaseMask != greaseValue {
|
||||
buf = strconv.AppendUint(buf, uint64(curve), 10)
|
||||
buf = append(buf, '-')
|
||||
n = 1
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:len(buf)-n]
|
||||
buf = append(buf, ',')
|
||||
n = 0
|
||||
|
||||
for _, point := range hello.SupportedPoints {
|
||||
buf = strconv.AppendUint(buf, uint64(point), 10)
|
||||
buf = append(buf, '-')
|
||||
n = 1
|
||||
}
|
||||
|
||||
buf = buf[:len(buf)-n]
|
||||
|
||||
sum := md5.Sum(buf)
|
||||
return TLSFingerprintJA3N(sum[:])
|
||||
}
|
||||
176
internal/fingerprint/ja4.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TLSFingerprintJA4 represents a JA4 fingerprint
|
||||
type TLSFingerprintJA4 struct {
|
||||
A [10]byte
|
||||
B [6]byte
|
||||
C [6]byte
|
||||
}
|
||||
|
||||
func (f *TLSFingerprintJA4) String() string {
|
||||
if f == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Join([]string{
|
||||
string(f.A[:]),
|
||||
hex.EncodeToString(f.B[:]),
|
||||
hex.EncodeToString(f.C[:]),
|
||||
}, "_")
|
||||
}
|
||||
|
||||
func buildJA4(hello *tls.ClientHelloInfo) (ja4 TLSFingerprintJA4) {
|
||||
buf := make([]byte, 0, 36)
|
||||
|
||||
hasQuic := false
|
||||
|
||||
for _, ext := range hello.Extensions {
|
||||
if ext == extensionQUICTransportParameters {
|
||||
hasQuic = true
|
||||
}
|
||||
}
|
||||
|
||||
switch hasQuic {
|
||||
case true:
|
||||
buf = append(buf, 'q')
|
||||
case false:
|
||||
buf = append(buf, 't')
|
||||
}
|
||||
|
||||
{
|
||||
var sslVersion uint16
|
||||
for _, v := range hello.SupportedVersions {
|
||||
if v&greaseMask != greaseValue {
|
||||
if v > sslVersion {
|
||||
sslVersion = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch sslVersion {
|
||||
case tls.VersionTLS10:
|
||||
buf = append(buf, '1', '0')
|
||||
case tls.VersionTLS11:
|
||||
buf = append(buf, '1', '1')
|
||||
case tls.VersionTLS12:
|
||||
buf = append(buf, '1', '2')
|
||||
case tls.VersionTLS13:
|
||||
buf = append(buf, '1', '3')
|
||||
default:
|
||||
sslVersion -= 0x0201
|
||||
buf = strconv.AppendUint(buf, uint64(sslVersion>>8), 10)
|
||||
buf = strconv.AppendUint(buf, uint64(sslVersion&0xff), 10)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if slices.Contains(hello.Extensions, extensionServerName) && hello.ServerName != "" {
|
||||
buf = append(buf, 'd')
|
||||
} else {
|
||||
buf = append(buf, 'i')
|
||||
}
|
||||
|
||||
ciphers := make([]uint16, 0, len(hello.CipherSuites))
|
||||
for _, cipher := range hello.CipherSuites {
|
||||
if cipher&greaseMask != greaseValue {
|
||||
ciphers = append(ciphers, cipher)
|
||||
}
|
||||
}
|
||||
|
||||
extensionCount := 0
|
||||
extensions := make([]uint16, 0, len(hello.Extensions))
|
||||
for _, extension := range hello.Extensions {
|
||||
if extension&greaseMask != greaseValue {
|
||||
extensionCount++
|
||||
if extension != extensionALPN && extension != extensionServerName {
|
||||
extensions = append(extensions, extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schemes := make([]tls.SignatureScheme, 0, len(hello.SignatureSchemes))
|
||||
|
||||
for _, scheme := range hello.SignatureSchemes {
|
||||
if scheme&greaseMask != greaseValue {
|
||||
schemes = append(schemes, scheme)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: maybe little endian
|
||||
slices.Sort(ciphers)
|
||||
slices.Sort(extensions)
|
||||
//slices.Sort(schemes)
|
||||
|
||||
if len(ciphers) < 10 {
|
||||
buf = append(buf, '0')
|
||||
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
|
||||
} else if len(ciphers) > 99 {
|
||||
buf = append(buf, '9', '9')
|
||||
} else {
|
||||
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
|
||||
}
|
||||
|
||||
if extensionCount < 10 {
|
||||
buf = append(buf, '0')
|
||||
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
|
||||
} else if extensionCount > 99 {
|
||||
buf = append(buf, '9', '9')
|
||||
} else {
|
||||
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
|
||||
}
|
||||
|
||||
if len(hello.SupportedProtos) > 0 && len(hello.SupportedProtos[0]) > 1 {
|
||||
buf = append(buf, hello.SupportedProtos[0][0], hello.SupportedProtos[0][len(hello.SupportedProtos[0])-1])
|
||||
} else {
|
||||
buf = append(buf, '0', '0')
|
||||
}
|
||||
|
||||
copy(ja4.A[:], buf)
|
||||
|
||||
ja4.B = ja4SHA256(uint16SliceToHex(ciphers))
|
||||
|
||||
extBuf := uint16SliceToHex(extensions)
|
||||
|
||||
if len(schemes) > 0 {
|
||||
extBuf = append(extBuf, '_')
|
||||
extBuf = append(extBuf, uint16SliceToHex(schemes)...)
|
||||
}
|
||||
|
||||
ja4.C = ja4SHA256(extBuf)
|
||||
|
||||
return ja4
|
||||
}
|
||||
|
||||
func uint16SliceToHex[T ~uint16](in []T) (out []byte) {
|
||||
if len(in) == 0 {
|
||||
return out
|
||||
}
|
||||
out = slices.Grow(out, hex.EncodedLen(len(in)*2)+len(in))
|
||||
|
||||
for _, n := range in {
|
||||
out = append(out, fmt.Sprintf("%04x", uint16(n))...)
|
||||
out = append(out, ',')
|
||||
}
|
||||
out = out[:len(out)-1]
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func ja4SHA256(buf []byte) [6]byte {
|
||||
if len(buf) == 0 {
|
||||
return [6]byte{0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
sum := sha256.Sum256(buf)
|
||||
|
||||
return [6]byte(sum[:6])
|
||||
}
|
||||
46
internal/fingerprint/tcp.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JA4T represents a TCP fingerprint
|
||||
type JA4T struct {
|
||||
Window uint32
|
||||
Options []uint8
|
||||
MSS uint16
|
||||
WindowScale uint8
|
||||
}
|
||||
|
||||
func (j JA4T) String() string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Start with the window size
|
||||
fmt.Fprintf(&sb, "%d", j.Window)
|
||||
|
||||
// Append each option
|
||||
for i, opt := range j.Options {
|
||||
if i == 0 {
|
||||
fmt.Fprint(&sb, "_")
|
||||
} else {
|
||||
fmt.Fprint(&sb, "-")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%d", opt)
|
||||
}
|
||||
|
||||
// Append MSS and WindowScale
|
||||
fmt.Fprintf(&sb, "_%d_%d", j.MSS, j.WindowScale)
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// GetTCPFingerprint extracts TCP fingerprint from HTTP request context
|
||||
func GetTCPFingerprint(r *http.Request) *JA4T {
|
||||
ptr := r.Context().Value(tcpFingerprintKey{})
|
||||
if fpPtr, ok := ptr.(*JA4T); ok && ptr != nil && fpPtr != nil {
|
||||
return fpPtr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
106
internal/fingerprint/tcp_freebsd.go
Normal file
@@ -0,0 +1,106 @@
|
||||
//go:build freebsd
|
||||
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type tcpInfo struct {
|
||||
State uint8
|
||||
Options uint8
|
||||
SndScale uint8
|
||||
RcvScale uint8
|
||||
__pad [4]byte
|
||||
Rto uint32
|
||||
Ato uint32
|
||||
SndMss uint32
|
||||
RcvMss uint32
|
||||
Unacked uint32
|
||||
Sacked uint32
|
||||
Lost uint32
|
||||
Retrans uint32
|
||||
Fackets uint32
|
||||
Last_data_sent uint32
|
||||
Last_ack_sent uint32
|
||||
Last_data_recv uint32
|
||||
Last_ack_recv uint32
|
||||
Pmtu uint32
|
||||
Rcv_ssthresh uint32
|
||||
RTT uint32
|
||||
RTTvar uint32
|
||||
Snd_ssthresh uint32
|
||||
Snd_cwnd uint32
|
||||
Advmss uint32
|
||||
Reordering uint32
|
||||
Rcv_rtt uint32
|
||||
Rcv_space uint32
|
||||
Total_retrans uint32
|
||||
Snd_wnd uint32
|
||||
// Truncated for brevity — add more fields if needed
|
||||
}
|
||||
|
||||
// AssignTCPFingerprint extracts TCP fingerprint information from a connection
|
||||
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a TCPConn")
|
||||
}
|
||||
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SyscallConn failed: %w", err)
|
||||
}
|
||||
|
||||
var info tcpInfo
|
||||
var sysErr error
|
||||
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
size := uint32(unsafe.Sizeof(info))
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_GETSOCKOPT,
|
||||
fd,
|
||||
uintptr(syscall.IPPROTO_TCP),
|
||||
uintptr(syscall.TCP_INFO),
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
0,
|
||||
)
|
||||
if errno != 0 {
|
||||
sysErr = errno
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SyscallConn.Control: %w", err)
|
||||
}
|
||||
if sysErr != nil {
|
||||
return nil, fmt.Errorf("getsockopt TCP_INFO: %w", sysErr)
|
||||
}
|
||||
|
||||
fp := &JA4T{
|
||||
Window: info.Snd_wnd,
|
||||
MSS: uint16(info.SndMss),
|
||||
WindowScale: info.SndScale,
|
||||
}
|
||||
|
||||
const (
|
||||
TCPI_OPT_TIMESTAMPS = 1 << 0
|
||||
TCPI_OPT_SACK = 1 << 1
|
||||
TCPI_OPT_WSCALE = 1 << 2
|
||||
)
|
||||
|
||||
if info.Options&TCPI_OPT_SACK != 0 {
|
||||
fp.Options = append(fp.Options, 4, 1)
|
||||
}
|
||||
if info.Options&TCPI_OPT_TIMESTAMPS != 0 {
|
||||
fp.Options = append(fp.Options, 8, 1)
|
||||
}
|
||||
if info.Options&TCPI_OPT_WSCALE != 0 {
|
||||
fp.Options = append(fp.Options, 3)
|
||||
}
|
||||
|
||||
return fp, nil
|
||||
}
|
||||
132
internal/fingerprint/tcp_linux.go
Normal file
@@ -0,0 +1,132 @@
|
||||
//go:build linux
|
||||
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type tcpInfo struct {
|
||||
State uint8
|
||||
Ca_state uint8
|
||||
Retransmits uint8
|
||||
Probes uint8
|
||||
Backoff uint8
|
||||
Options uint8
|
||||
Wnd_scale uint8
|
||||
Delivery_rate_app_limited uint8
|
||||
|
||||
Rto uint32
|
||||
Ato uint32
|
||||
SndMss uint32
|
||||
RcvMss uint32
|
||||
|
||||
Unacked uint32
|
||||
Sacked uint32
|
||||
Lost uint32
|
||||
Retrans uint32
|
||||
Fackets uint32
|
||||
|
||||
Last_data_sent uint32
|
||||
Last_ack_sent uint32
|
||||
Last_data_recv uint32
|
||||
Last_ack_recv uint32
|
||||
PMTU uint32
|
||||
Rcv_ssthresh uint32
|
||||
RTT uint32
|
||||
RTTvar uint32
|
||||
Snd_ssthresh uint32
|
||||
Snd_cwnd uint32
|
||||
Advmss uint32
|
||||
Reordering uint32
|
||||
Rcv_rtt uint32
|
||||
Rcv_space uint32
|
||||
Total_retrans uint32
|
||||
Pacing_rate uint64
|
||||
Max_pacing_rate uint64
|
||||
Bytes_acked uint64
|
||||
Bytes_received uint64
|
||||
Segs_out uint32
|
||||
Segs_in uint32
|
||||
Notsent_bytes uint32
|
||||
Min_rtt uint32
|
||||
Data_segs_in uint32
|
||||
Data_segs_out uint32
|
||||
Delivery_rate uint64
|
||||
Busy_time uint64
|
||||
Rwnd_limited uint64
|
||||
Sndbuf_limited uint64
|
||||
Delivered uint32
|
||||
Delivered_ce uint32
|
||||
Bytes_sent uint64
|
||||
Bytes_retrans uint64
|
||||
DSACK_dups uint32
|
||||
Reord_seen uint32
|
||||
Rcv_ooopack uint32
|
||||
Snd_wnd uint32
|
||||
}
|
||||
|
||||
// AssignTCPFingerprint extracts TCP fingerprint information from a connection
|
||||
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a TCPConn")
|
||||
}
|
||||
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SyscallConn failed: %w", err)
|
||||
}
|
||||
|
||||
var info tcpInfo
|
||||
var sysErr error
|
||||
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
size := uint32(unsafe.Sizeof(info))
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_GETSOCKOPT,
|
||||
fd,
|
||||
uintptr(syscall.IPPROTO_TCP),
|
||||
uintptr(syscall.TCP_INFO),
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
0,
|
||||
)
|
||||
if errno != 0 {
|
||||
sysErr = errno
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SyscallConn.Control: %w", err)
|
||||
}
|
||||
if sysErr != nil {
|
||||
return nil, fmt.Errorf("getsockopt TCP_INFO: %w", sysErr)
|
||||
}
|
||||
|
||||
fp := &JA4T{
|
||||
Window: info.Snd_wnd,
|
||||
MSS: uint16(info.SndMss),
|
||||
}
|
||||
|
||||
const (
|
||||
TCPI_OPT_TIMESTAMPS = 1 << 0
|
||||
TCPI_OPT_SACK = 1 << 1
|
||||
TCPI_OPT_WSCALE = 1 << 2
|
||||
)
|
||||
|
||||
if info.Options&TCPI_OPT_SACK != 0 {
|
||||
fp.Options = append(fp.Options, 4, 1)
|
||||
}
|
||||
if info.Options&TCPI_OPT_TIMESTAMPS != 0 {
|
||||
fp.Options = append(fp.Options, 8, 1)
|
||||
}
|
||||
if info.Options&TCPI_OPT_WSCALE != 0 {
|
||||
fp.Options = append(fp.Options, 3)
|
||||
fp.WindowScale = info.Wnd_scale
|
||||
}
|
||||
|
||||
return fp, nil
|
||||
}
|
||||
11
internal/fingerprint/tcp_unsupported.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !linux && !freebsd
|
||||
|
||||
package fingerprint
|
||||
|
||||
import "net"
|
||||
|
||||
// AssignTCPFingerprint is not supported on this platform
|
||||
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
|
||||
// Not supported on macOS and other platforms
|
||||
return &JA4T{}, nil
|
||||
}
|
||||
110
internal/fingerprint/tls.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// ApplyTLSFingerprinter configures a TLS server to capture TLS fingerprints
|
||||
func ApplyTLSFingerprinter(server *http.Server) {
|
||||
if server.TLSConfig == nil {
|
||||
return
|
||||
}
|
||||
server.TLSConfig = server.TLSConfig.Clone()
|
||||
|
||||
getConfigForClient := server.TLSConfig.GetConfigForClient
|
||||
|
||||
if getConfigForClient == nil {
|
||||
getConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
server.TLSConfig.GetConfigForClient = func(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
ja3n, ja4 := buildTLSFingerprint(clientHello)
|
||||
ptr := clientHello.Context().Value(tlsFingerprintKey{})
|
||||
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
|
||||
fpPtr.ja3n.Store(&ja3n)
|
||||
fpPtr.ja4.Store(&ja4)
|
||||
}
|
||||
return getConfigForClient(clientHello)
|
||||
}
|
||||
server.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
|
||||
ctx = context.WithValue(ctx, tlsFingerprintKey{}, &TLSFingerprint{})
|
||||
|
||||
if tc, ok := c.(*tls.Conn); ok {
|
||||
tcpFP, err := AssignTCPFingerprint(tc.NetConn())
|
||||
if err == nil {
|
||||
ctx = context.WithValue(ctx, tcpFingerprintKey{}, tcpFP)
|
||||
} else {
|
||||
slog.Debug("ja4t error", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
type tcpFingerprintKey struct{}
|
||||
type tlsFingerprintKey struct{}
|
||||
|
||||
// TLSFingerprint represents TLS fingerprint data
|
||||
type TLSFingerprint struct {
|
||||
ja3n atomic.Pointer[TLSFingerprintJA3N]
|
||||
ja4 atomic.Pointer[TLSFingerprintJA4]
|
||||
}
|
||||
|
||||
// JA3N returns the JA3N fingerprint
|
||||
func (f *TLSFingerprint) JA3N() *TLSFingerprintJA3N {
|
||||
return f.ja3n.Load()
|
||||
}
|
||||
|
||||
// JA4 returns the JA4 fingerprint
|
||||
func (f *TLSFingerprint) JA4() *TLSFingerprintJA4 {
|
||||
return f.ja4.Load()
|
||||
}
|
||||
|
||||
const greaseMask = 0x0F0F
|
||||
const greaseValue = 0x0a0a
|
||||
|
||||
// TLS extension numbers
|
||||
const (
|
||||
extensionServerName uint16 = 0
|
||||
extensionStatusRequest uint16 = 5
|
||||
extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7
|
||||
extensionSupportedPoints uint16 = 11
|
||||
extensionSignatureAlgorithms uint16 = 13
|
||||
extensionALPN uint16 = 16
|
||||
extensionSCT uint16 = 18
|
||||
extensionExtendedMasterSecret uint16 = 23
|
||||
extensionSessionTicket uint16 = 35
|
||||
extensionPreSharedKey uint16 = 41
|
||||
extensionEarlyData uint16 = 42
|
||||
extensionSupportedVersions uint16 = 43
|
||||
extensionCookie uint16 = 44
|
||||
extensionPSKModes uint16 = 45
|
||||
extensionCertificateAuthorities uint16 = 47
|
||||
extensionSignatureAlgorithmsCert uint16 = 50
|
||||
extensionKeyShare uint16 = 51
|
||||
extensionQUICTransportParameters uint16 = 57
|
||||
extensionRenegotiationInfo uint16 = 0xff01
|
||||
extensionECHOuterExtensions uint16 = 0xfd00
|
||||
extensionEncryptedClientHello uint16 = 0xfe0d
|
||||
)
|
||||
|
||||
func buildTLSFingerprint(hello *tls.ClientHelloInfo) (TLSFingerprintJA3N, TLSFingerprintJA4) {
|
||||
return TLSFingerprintJA3N(buildJA3N(hello, true)), buildJA4(hello)
|
||||
}
|
||||
|
||||
// GetTLSFingerprint extracts TLS fingerprint from HTTP request context
|
||||
func GetTLSFingerprint(r *http.Request) *TLSFingerprint {
|
||||
ptr := r.Context().Value(tlsFingerprintKey{})
|
||||
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
|
||||
return fpPtr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
internal/health.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/health"
|
||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
var HealthSrv = health.NewServer()
|
||||
|
||||
func SetHealth(svc string, status healthv1.HealthCheckResponse_ServingStatus) {
|
||||
HealthSrv.SetServingStatus(svc, status)
|
||||
}
|
||||
|
||||
func GetHealth(svc string) (healthv1.HealthCheckResponse_ServingStatus, bool) {
|
||||
st, err := HealthSrv.Check(context.Background(), &healthv1.HealthCheckRequest{
|
||||
Service: svc,
|
||||
})
|
||||
if err != nil {
|
||||
return healthv1.HealthCheckResponse_UNKNOWN, false
|
||||
}
|
||||
|
||||
return st.GetStatus(), true
|
||||
}
|
||||
14
internal/ja4h.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/lum8rjack/go-ja4h"
|
||||
)
|
||||
|
||||
func JA4H(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Add("X-Http-Fingerprint-JA4H", ja4h.JA4H(r))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -60,15 +60,6 @@ func New(ctx context.Context, thothURL, apiToken string, plaintext bool) (*Clien
|
||||
|
||||
hc := healthv1.NewHealthClient(conn)
|
||||
|
||||
resp, err := hc.Check(ctx, &healthv1.HealthCheckRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't verify thoth health at %s: %w", thothURL, err)
|
||||
}
|
||||
|
||||
if resp.Status != healthv1.HealthCheckResponse_SERVING {
|
||||
return nil, fmt.Errorf("thoth is not healthy, wanted %s but got %s", healthv1.HealthCheckResponse_SERVING, resp.Status)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
conn: conn,
|
||||
health: hc,
|
||||
|
||||
@@ -394,8 +394,6 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
|
||||
|
||||
redir := r.FormValue("redir")
|
||||
redirURL, err := url.ParseRequestURI(redir)
|
||||
if err != nil {
|
||||
|
||||
@@ -131,6 +131,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
chall, err := s.challengeFor(r)
|
||||
if err != nil {
|
||||
lg.Error("can't get challenge", "err", "err")
|
||||
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
|
||||
return
|
||||
}
|
||||
@@ -155,6 +156,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||
if !ok {
|
||||
lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm)
|
||||
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
|
||||
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
|
||||
return
|
||||
}
|
||||
@@ -196,7 +198,7 @@ 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) {
|
||||
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)
|
||||
templ.Handler(web.Base(localizer.T("oh_noes"), web.ErrorPage(msg, s.opts.WebmasterEmail, r.FormValue("redir"), localizer), s.policy.Impressum, localizer), templ.WithStatus(status)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
64
lib/localization/locales/cs.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"loading": "Načítám...",
|
||||
"why_am_i_seeing": "Proč to vidím?",
|
||||
"protected_by": "Chráněno pomocí",
|
||||
"protected_from": "Od",
|
||||
"made_with": "Vytvořeno s ❤️ v 🇨🇦",
|
||||
"mascot_design": "Design maskota od",
|
||||
"ai_companies_explanation": "Vidíte to proto, že správce této webové stránky nastavil Anubis na ochranu serveru před pohromou AI společností agresivně stahujících webové stránky. To může, a také způsobuje výpadky webových stránek, což je činí nepřístupnými pro všechny.",
|
||||
"anubis_compromise": "Anubis je kompromis. Anubis používá schéma Proof-of-Work v duchu Hashcash, návrhu schématu proof-of-work pro snížení e-mailového spamu. Myšlenka je, že na individuálních úrovních je dodatečná zátěž zanedbatelná, ale na úrovni masového použití se sčítá a činí stahování mnohem dražším.",
|
||||
"hack_purpose": "Ve výsledku je to hack, jehož skutečným účelem je poskytnout \"dostatečně dobré\" prozatímní řešení, které nám poskytuje čas pracovat na ověřování a identifikaci robotů (např. prostřednictvím toho, jak vykreslují fonty), tak aby stránka s výzvou proof of work nemusela být prezentována uživatelům, kteří jsou legitimní.",
|
||||
"jshelter_note": "Upozorňujeme, že Anubis vyžaduje použití moderních funkcí JavaScriptu, které rozšíření jako JShelter omezují. Prosím zakažte JShelter nebo jiná podobná rozšíření pro tuto doménu.",
|
||||
"version_info": "Tato webová stránka běží na Anubis verzi",
|
||||
"try_again": "Zkusit znovu",
|
||||
"go_home": "Přejít na úvodní stránku",
|
||||
"contact_webmaster": "nebo pokud si myslíte, že byste neměli být blokováni, kontaktujte správce na",
|
||||
"connection_security": "Prosím počkejte chvilku, zatímco zajišťujeme bezpečnost vašeho připojení.",
|
||||
"javascript_required": "Bohužel musíte povolit JavaScript, abyste prošli touto výzvou. To je vyžadováno proto, že AI společnosti změnily společenskou smlouvu ohledně toho, jak funguje hosting webových stránek. Řešení bez JavaScriptu je ve vývoji.",
|
||||
"benchmark_requires_js": "Spuštění testovacího nástroje vyžaduje povolení JavaScriptu.",
|
||||
"difficulty": "Obtížnost:",
|
||||
"algorithm": "Algoritmus:",
|
||||
"compare": "Porovnat:",
|
||||
"time": "Čas",
|
||||
"iters": "Iterace",
|
||||
"time_a": "Čas A",
|
||||
"iters_a": "Iterace A",
|
||||
"time_b": "Čas B",
|
||||
"iters_b": "Iterace B",
|
||||
"static_check_endpoint": "Toto je pouze kontrolní bod pro přístup na tuto stránku.",
|
||||
"authorization_required": "Vyžadována autorizace",
|
||||
"cookies_disabled": "Váš prohlížeč je nakonfigurován tak, aby zakázal cookies. Anubis vyžaduje cookies pro legitimní zájem zajistit, že jste legitimní uživatel. Prosím povolte cookies pro tuto doménu",
|
||||
"access_denied": "Přístup zamítnut: kód chyby",
|
||||
"dronebl_entry": "DroneBL nahlásil záznam",
|
||||
"see_dronebl_lookup": "viz",
|
||||
"internal_server_error": "Interní chyba serveru: správce špatně nakonfiguroval Anubis. Kontaktujte správce a požádejte ho, aby se do systémových záznamů",
|
||||
"invalid_redirect": "Neplatné přesměrování",
|
||||
"redirect_not_parseable": "URL přesměrování nelze analyzovat",
|
||||
"redirect_domain_not_allowed": "Doména přesměrování není povolena",
|
||||
"failed_to_sign_jwt": "nepodařilo se podepsat JWT",
|
||||
"invalid_invocation": "Neplatné vyvolání MakeChallenge",
|
||||
"client_error_browser": "Chyba prohlížeče: Ujistěte se, že váš prohlížeč je aktuální a zkuste to později.",
|
||||
"oh_noes": "Ale ne!",
|
||||
"benchmarking_anubis": "Testování Anubise!",
|
||||
"you_are_not_a_bot": "Nejste robot!",
|
||||
"making_sure_not_bot": "Ujišťujeme se, že nejste robot!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Váš prohlížeč nepodporuje funkci web.crypto. Používáte zabezpečené připojení?",
|
||||
"js_web_workers_error": "Váš prohlížeč nepodporuje web workers (Anubis je používá, aby zabránil zamrznutí vašeho prohlížeče). Máte nainstalovano rozšíření JShelter nebo podobné?",
|
||||
"js_cookies_error": "Váš prohlížeč neukládá cookies. Anubis používá cookies k určení, kteří klienti prošli výzvami uložením podepsaného tokenu v cookie. Prosím povolte ukládání cookies pro tuto doménu. Názvy cookies, které Anubis ukládá, se mohou měnit bez upozornění. Názvy a hodnoty cookies nejsou součástí veřejného API.",
|
||||
"js_context_not_secure": "Váše připojení není bezpečné!",
|
||||
"js_context_not_secure_msg": "Zkuste se připojit přes HTTPS nebo informujte správce o nastavení HTTPS. Pro více informací viz <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Počítám...",
|
||||
"js_missing_feature": "Chybějící funkce",
|
||||
"js_challenge_error": "Chyba výzvy!",
|
||||
"js_challenge_error_msg": "Nepodařilo se vyřešit kontrolní algoritmus. Možná budete chtít obnovit stránku.",
|
||||
"js_calculating_difficulty": "Počítám...<br/>Obtížnost:",
|
||||
"js_speed": "Rychlost:",
|
||||
"js_verification_longer": "Ověřování trvá déle, než se očekávalo. Prosím neobnovujte stránku.",
|
||||
"js_success": "Úspěch!",
|
||||
"js_done_took": "Hotovo! Trvalo to",
|
||||
"js_iterations": "iterací",
|
||||
"js_finished_reading": "Dokončil jsem čtení, pokračovat →",
|
||||
"js_calculation_error": "Chyba výpočtu!",
|
||||
"js_calculation_error_msg": "Nepodařilo se vypočítat výzvu:"
|
||||
}
|
||||
@@ -2,19 +2,20 @@
|
||||
"loading": "Ladevorgang...",
|
||||
"why_am_i_seeing": "Warum sehe ich diese Seite?",
|
||||
"protected_by": "Geschützt durch",
|
||||
"protected_from": "From",
|
||||
"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",
|
||||
"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 niemanden erreichbar ist.",
|
||||
"anubis_compromise": "Anubis stellt einen Kompromiss dar. Es verwendet eine Proof-of-Work-Methode nach Hashcash, die ursprünglich zur Bekämpfung von E-Mail-Spam entwickelt wurde. Die Idee dahinter ist, dass ein legitimer Besucher die Webseite mit einer vernachlässigbaren Verzögerung erreichen kann. Massenhaftes Scraping wird dadurch jedoch aufwändig und teuer.",
|
||||
"hack_purpose": "Man könnte dies als eine Lösung bezeichnen, die einem etwas Zeit für Fingerprinting und dem Identifizieren von Headless-Browsern verschafft. Besucher, die mit sehr hoher Wahrscheinlichkeit legitim sind, bekommen diese Seite nicht zu sehen.",
|
||||
"jshelter_note": "Anubis benötigt moderne JavaScript-Features, welche von Plugins wie JShelter deaktiviert werden. Bitte deaktiviere JShelter oder ähnliche Plugins für diese Domain.",
|
||||
"version_info": "Diese Webseite läuft mit der Anubis-Version",
|
||||
"try_again": "Erneut versuchen",
|
||||
"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.",
|
||||
"contact_webmaster": "oder wenn Du glaubst, dass es sich hierbei 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": "Du musst JavaScript aktivieren, um diese Prüfung durchführen zu können. Dies ist notwendig, da KI-Unternehmen den sozialen Vertrag bezüglich des Hostings von Webseiten gebrochen haben. Eine Lösung ohne JavaScript ist in Entwicklung.",
|
||||
"benchmark_requires_js": "Für die Nutzung des Benchmark-Tools muss JavaScript aktiviert werden.",
|
||||
"difficulty": "Schwierigkeit:",
|
||||
"algorithm": "Algorithmus:",
|
||||
"compare": "Vergleich:",
|
||||
@@ -24,40 +25,40 @@
|
||||
"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.",
|
||||
"static_check_endpoint": "Dies ist nur ein Endpunkt, der von 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.",
|
||||
"cookies_disabled": "Cookies sind in deinem Browser deaktiviert. Anubis benötigt Cookies, um sicherzustellen, dass es sich hierbei um einen legitimen 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.",
|
||||
"internal_server_error": "Interner Server-Fehler: Der Administrator hat Anubis fehlerhaft konfiguriert. Bitte kontaktiere den Administrator und bitte ihn, die Logs zu prüfen.",
|
||||
"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!",
|
||||
"failed_to_sign_jwt": "JWT konnte nicht signiert werden",
|
||||
"invalid_invocation": "Ungültiger Aufruf von MakeChallenge",
|
||||
"client_error_browser": "Client-Fehler: Bitte stelle sicher, dass dein Browser aktuell ist und versuche es später erneut.",
|
||||
"oh_noes": "Oh nein!",
|
||||
"benchmarking_anubis": "Benchmark wird durchgeführt!",
|
||||
"you_are_not_a_bot": "Sie sind kein Bot!",
|
||||
"making_sure_not_bot": "Ihr Browser wird geprüft!",
|
||||
"you_are_not_a_bot": "Du bist kein Bot!",
|
||||
"making_sure_not_bot": "Dein 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_web_crypto_error": "Dein Browser hat kein funktionierendes web.crypto Element. Wird eine sichere Verbindung verwendet?",
|
||||
"js_web_workers_error": "Dein Browser unterstützt keine Web-Worker (Anubis verwendet diese, damit der Browser nicht einfriert). Ist ein Plugin wie JShelter installiert?",
|
||||
"js_cookies_error": "Dein Browser speichert keine Cookies. Anubis verwendet Cookies, um nach bestandener Prüfung ein signiertes Token abzulegen. Bitte aktiviere Cookies für diese Domain. Die Cookie-Namen von Anubis könnten sich jederzeit ändern. Cookie-Namen und die gespeicherten Werte 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_context_not_secure_msg": "Bitte versuche, dich via HTTPS zu verbinden oder weise den Administrator darauf hin, 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_challenge_error": "Prüfung fehlgeschlagen!",
|
||||
"js_challenge_error_msg": "Der Prüf-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_verification_longer": "Die Prüfung benötigt länger als erwartet. Bitte bleibe auf der Seite und lade diese nicht neu.",
|
||||
"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:"
|
||||
"js_calculation_error": "Berechnung fehlgeschlagen!",
|
||||
"js_calculation_error_msg": "Fehler bei der Berechnung der Prüfung:"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"loading": "Loading...",
|
||||
"why_am_i_seeing": "Why am I seeing this?",
|
||||
"protected_by": "Protected by",
|
||||
"protected_from": "From",
|
||||
"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.",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"loading": "Cargando...",
|
||||
"why_am_i_seeing": "¿Por qué veo esto?",
|
||||
"protected_by": "Protegido por",
|
||||
"protected_from": "From",
|
||||
"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.",
|
||||
|
||||
64
lib/localization/locales/et.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"loading": "Laadin...",
|
||||
"why_am_i_seeing": "Miks ma pean seda nägema?",
|
||||
"protected_by": "Kaitseb",
|
||||
"protected_from": "From",
|
||||
"made_with": "Tehtud ❤️ga 🇨🇦s",
|
||||
"mascot_design": "Maskoti disainis",
|
||||
"ai_companies_explanation": "Seda näidatakse selle pärast, et selle lehe administraator on paigaldanud Anubise, et kaitsta serverit selle nuhtluse eest, mida kujutab endast AI firmade agressiivne veebikraapimine. Selle tagajärjeks võib olla ja tihti ongi see, et veebilehed lakkavad töötamast ja keegi ei saa nendele ligi.",
|
||||
"anubis_compromise": "Anubis on kompromisslahendus. Anubis kasutab nö. töötõendi skeemi, mille sarnane oli <em>Hashcash</em>, mis oli mõeldud spämmikaitseks. Põhimõte on selles, et üksiku kasutaja tasemel on lisakoormus tajumatu, aga massiivse kraapimise tasemel see koormus läheb kõik arvesse ja muudab andmete töötluse palju kallimaks.",
|
||||
"hack_purpose": "Lõppudelõpuks on see siiski rutuga tehtud lahendus, mille tegelik eesmärk on olla \"piisavalt hea\", et oleks rohkem aega leida viise, kuidas saaks tuvastada brauseriautomaate (näiteks sellest, kuidas nad fonte joonistavad), et saavutada olukord, kus kontrollekraani ei olegi vaja näidata kasutajatele, kes on suurema tõenäosusega päriselt inimesed.",
|
||||
"jshelter_note": "NB! Anubis vajab töötamiseks kaasaegseid JavaScripti võimalusi, mida teatud pluginad nagu JShelter ära keelavad. Palun lülita JShelter või teised sellised veebilehitseja laiendused välja.",
|
||||
"version_info": "Sellel lehel jookseb Anubis, versioon",
|
||||
"try_again": "Proovi uuesti",
|
||||
"go_home": "Mine koju",
|
||||
"contact_webmaster": "või kui sa arvad, et sa ei peaks olema blokeeritud, võta ühendust veebimeistriga aadressil",
|
||||
"connection_security": "Oota korraks, me kontrollime ühenduse turvalisust.",
|
||||
"javascript_required": "Kahjuks tuleb JavaScript sisse lülitada, et sellest kontrollist mööda pääseda. See on kohustuslik, sest AI ettevõtted on muutnud ühiskondlikke norme veebimajutuse suhtes. Ilma JavaScriptita töötav versioon on alles arendamisel.",
|
||||
"benchmark_requires_js": "Kiirustesti jaoks on vajalik JavaScript sisse lülitada.",
|
||||
"difficulty": "Raskus:",
|
||||
"algorithm": "Algoritm:",
|
||||
"compare": "Võrdle:",
|
||||
"time": "Aega",
|
||||
"iters": "Korda",
|
||||
"time_a": "A aega",
|
||||
"iters_a": "A korda",
|
||||
"time_b": "B aega",
|
||||
"iters_b": "B korda",
|
||||
"static_check_endpoint": "Seda lehte vaatab ainult sinu vaheserver.",
|
||||
"authorization_required": "Ligipääs puudub",
|
||||
"cookies_disabled": "Sinu brauseris on küpsised keelatud. Anubis vajab küpsiseid töötamiseks, et aru saada, kas sa oled päris kasutaja või mitte. Palun luba küpsised sellel domeenil",
|
||||
"access_denied": "Ligipääs keelatud: veakood",
|
||||
"dronebl_entry": "DroneBL tagastas sissekande",
|
||||
"see_dronebl_lookup": "vaata",
|
||||
"internal_server_error": "Programmi sisemine viga: administraator on Anubise valesti seadistanud. Võta temaga ühendust ja palu tal otsida logidest märksõna",
|
||||
"invalid_redirect": "Vigane ümbersuunamine",
|
||||
"redirect_not_parseable": "Ümbersuunamise URL on vigane",
|
||||
"redirect_domain_not_allowed": "Ümbersuunamise domeen pole lubatud",
|
||||
"failed_to_sign_jwt": "JWT allkirjastamine ebaõnnestus",
|
||||
"invalid_invocation": "MakeChallenge väljakutsumine on vigane",
|
||||
"client_error_browser": "Kliendipoolne viga: palun kontrolli, et su brauser oleks uuendatud ja proovi uuesti.",
|
||||
"oh_noes": "Oi ei!",
|
||||
"benchmarking_anubis": "Anubise kiirustest!",
|
||||
"you_are_not_a_bot": "Sina ei ole bott!",
|
||||
"making_sure_not_bot": "Kontrollime, et sa ei ole bott!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Sinu brauseris ei ole töötavat web.crypto elementi. Kas sa avasid selle turvakontekstis?",
|
||||
"js_web_workers_error": "Sinu brauser ei toeta veebi taustaprotsesse (Anubis kasutab neid, et su veebilehitseja ei hanguks). Kas sul on installitud mingi laiendus nagu JShelter?",
|
||||
"js_cookies_error": "Sinu brauser ei salvesta küpsiseid. Anubis kirjutab küpsise, milles on allkirjastatud sedel, et vahet teha, millised kliendid on kontrolli läbinud ja millised mitte. Palun luba küpsiste salvestamine sellel domeenil. Küpsiste nimed, mida Anubis kasutab, võivad muutuda ette teatamata. Küpsiste nimed ja väärtused ei ole avaliku liidese osa.",
|
||||
"js_context_not_secure": "Sinu brauserikontekst ei ole turvaline!",
|
||||
"js_context_not_secure_msg": "Proovi ühendada HTTPS aadressiga või anna administraatorile teada, et HTTPS on vajalik seadistada. Lisainfot vaata <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDNist</a>.",
|
||||
"js_calculating": "Arvutan...",
|
||||
"js_missing_feature": "Puuduv brauseri omadus",
|
||||
"js_challenge_error": "Kontrolli viga!",
|
||||
"js_challenge_error_msg": "Ei suutnud tuvastada kontrollalgoritmi. Võiksid proovida lehe uuesti laadida.",
|
||||
"js_calculating_difficulty": "Arvutan...<br/>Raskus:",
|
||||
"js_speed": "Kiirus:",
|
||||
"js_verification_longer": "Kontrollimine võtab kauem kui tavaliselt. Palun ära lae lehte uuesti.",
|
||||
"js_success": "Õnnestus!",
|
||||
"js_done_took": "Tehtud! Võttis",
|
||||
"js_iterations": "kordust",
|
||||
"js_finished_reading": "Lugesin ära, edasi →",
|
||||
"js_calculation_error": "Arvutamise viga!",
|
||||
"js_calculation_error_msg": "Ei suutnud kontrolli arvutada:"
|
||||
}
|
||||
65
lib/localization/locales/fil.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"loading": "Naglo-load...",
|
||||
"why_am_i_seeing": "Bakit nakikita ko ito?",
|
||||
"protected_by": "Pinoprotekta ng",
|
||||
"protected_from": "mula sa",
|
||||
"made_with": "Ginawa na may ❤️ sa 🇨🇦",
|
||||
"mascot_design": "Disenyo ng Maskot ni/ng",
|
||||
"ai_companies_explanation": "Nakikita mo ito dahil ang tagapangasiwa ng website na ito ay nag-set up ng Anubis upang protektahan ang server laban sa salot ng mga kumpanya ng AI na aggresibong nagse-scrape ng mga website. Maaari nitong magdulot ng downtime para sa mga website, na gagawing hindi naa-access ang kanilang mga resource para sa lahat.",
|
||||
"anubis_compromise": "Isang kompromiso ang Anubis. Gumagamit ang Anubis ng isang Proof-of-Work na scheme sa ugat ng Hashcash, isang iminungkahing proof-of-work scheme upang mabawasan ang email spam. Ang ideya ay sa indibidwal na scale hindi napapansin ang karagdagang load, ngunit sa malaking antas ng pag-scrape nagkararagdag ito at ginagawang mas mahal ang pag-scrape.",
|
||||
"hack_purpose": "Sa huli, ito ay isang hack na ang totoong layunin ay magbigay ng \"sapat na mabuti\" na placeholder na solusyon upang mas maraming oras ang magugugol sa pag-fingerprint at pagtukoy ng mga walang ulo na browser (hal: sa pamamagitan ng kung paano nila ginagawa ang pag-render ng font) nang sa gayon ay hindi na kailangang iharap sa mga user na mas malamang na maging lehitimo ang patunay ng hamon sa pahina ng trabaho.",
|
||||
"jshelter_note": "Pakitandaan na kinakailangan ng Anubis ang paggamit ng modernong JavaScript na feature na idi-disable ng mga plugin tulad ng JShelter. Mangyaring i-disable ang JShelter o ibang mga plugin para sa domain na ito.",
|
||||
"version_info": "Ang website na ito ay tumatakbo ng Anubis bersyon",
|
||||
"try_again": "Subukan muli",
|
||||
"go_home": "Bumalik sa panimula",
|
||||
"contact_webmaster": "o kung naniniwala ka na hindi ka dapat na-block, mangyaring makipag-ugnayan sa mga webmaster sa",
|
||||
"connection_security": "Mangyaring maghintay nang ilang sandali habang sinisigurado namin ang seguridad ng iyong koneksyon.",
|
||||
"javascript_required": "Nakalulungkot, ngunit kailangan mong paganahin ang JavaScript upang malampasan ang hamong ito. Ito ay kinakailangan dahil binago ng mga kumpanya ng AI ang social contract tungkol sa kung paano gumagana ang pagho-host ng website. Ang isang walang-JS na solusyon ay isang work-in-progress.",
|
||||
"benchmark_requires_js": "Kinakailangang naka-enable ang JavaScript upang patakbuhin ang benchmark tool.",
|
||||
"difficulty": "Kahirapan:",
|
||||
"algorithm": "Algoritmo:",
|
||||
"compare": "Kumpara:",
|
||||
"time": "Oras",
|
||||
"iters": "Mga Iterasyon",
|
||||
"time_a": "Time A",
|
||||
"iters_a": "Iters A",
|
||||
"time_b": "Time B",
|
||||
"iters_b": "Iters B",
|
||||
"static_check_endpoint": "Isa lang itong check endpoint para magamit ng iyong reverse proxy.",
|
||||
"authorization_required": "Kinakailangan ang pagpapatunay",
|
||||
"cookies_disabled": "Ang iyong browser ay na-configure upang hindi paganahin ang cookies. Kinakailangan ng Anubis ang cookies para sa lehitimong interes ng pagtiyak na ikaw ay isang wastong kliyente. Mangyaring paganahin ang cookies para sa domain na ito",
|
||||
"access_denied": "Tinanggihan ang Access: error code",
|
||||
"dronebl_entry": "Nag-ulat ang DroneBL ng entry",
|
||||
"see_dronebl_lookup": "tignan ang",
|
||||
"internal_server_error": "Internal Server Error: hindi na-configure nang mabuti ng tagapangasiwa ang Anubis. Makipag-ugnayan sa tagapangasiwa at sabihin sa kanila na tumingin sa mga log sa paligid ng",
|
||||
"invalid_redirect": "Hindi wastong redirect",
|
||||
"redirect_not_parseable": "Hindi ma-parse ang redirect URL",
|
||||
"redirect_domain_not_allowed": "Hindi pinapayagan ang redirect domain",
|
||||
"failed_to_sign_jwt": "nabigong ilagda ang JWT",
|
||||
"invalid_invocation": "Hindi wastong panawagan para sa MakeChallenge",
|
||||
"client_error_browser": "Error sa Kliyente: Pakitiyak na napapanahon ang iyong browser at subukang muli sa ibang pagkakataon.",
|
||||
"oh_noes": "Ay, naku!",
|
||||
"benchmarking_anubis": "Binebenchmark ang Anubis!",
|
||||
"you_are_not_a_bot": "Hindi ka isang bot!",
|
||||
"making_sure_not_bot": "Sinisigurado na hindi ka isang bot!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Ang iyong browser ay walang gumaganang web.crypto element. Tinitingnan mo ba ito sa isang secure na konteksto?",
|
||||
"js_web_workers_error": "Hindi sinusuportahan ng iyong browser ang mga web worker (ginagamit ito ng Anubis upang maiwasan ang pag-freeze ng iyong browser). Mayroon ka bang naka-install na plugin tulad ng JShelter?",
|
||||
"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 w",
|
||||
"js_cookies_error": "Ang iyong browser ay hindi nag-iimbak ng cookies. Gumagamit ang Anubis ng cookies upang matukoy kung aling mga kliyente ang nakapasa sa mga hamon sa pamamagitan ng pag-iimbak ng isang nilagdaang token sa isang cookie. Mangyaring paganahin ang pag-iimbak ng cookies para sa domain na ito. Ang mga pangalan ng cookies na Anubis store ay maaaring mag-iba nang walang abiso. Ang mga pangalan at value ng cookie ay hindi bahagi ng pampublikong API.",
|
||||
"js_context_not_secure": "Hindi secure ang iyong konteksto!",
|
||||
"js_context_not_secure_msg": "Subukang kumonekta sa pamamagitan ng HTTPS o sabihin sa admin na i-set up ang HTTPS. Para sa karagdagang impormasyon, tignan ang <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Kinakalkula...",
|
||||
"js_missing_feature": "Nawawalang feature",
|
||||
"js_challenge_error": "Error sa hamon!",
|
||||
"js_challenge_error_msg": "Nabigong iresolba ang algoritmo ng pagsusuri. Baka gusto mong i-reload ang pahina.",
|
||||
"js_calculating_difficulty": "Kinakalkula...<br/>Kahirapan:",
|
||||
"js_speed": "Bilis:",
|
||||
"js_verification_longer": "Mas tumatagal ang pag-verify kaysa sa inaasahan. Mangyaring huwag i-refresh ang pahina.",
|
||||
"js_success": "Matagumpay!",
|
||||
"js_done_took": "Tapos na! Nagtagal nang",
|
||||
"js_iterations": "mga iterasyon",
|
||||
"js_finished_reading": "Tapos na akong magbasa, magpatuloy →",
|
||||
"js_calculation_error": "Error sa pagkalkula!",
|
||||
"js_calculation_error_msg": "Nabigong ikalkula ang hamon:"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"loading": "Chargement...",
|
||||
"why_am_i_seeing": "Pourquoi je vois ceci ?",
|
||||
"protected_by": "Protégé par",
|
||||
"protected_from": "From",
|
||||
"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.",
|
||||
|
||||
64
lib/localization/locales/is.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"loading": "Hleður...",
|
||||
"why_am_i_seeing": "Af hverju er ég að sjá þetta?",
|
||||
"protected_by": "Verndað með",
|
||||
"protected_from": "From",
|
||||
"made_with": "Gert í 🇨🇦 með ❤️",
|
||||
"mascot_design": "Lukkudýrið hannað af",
|
||||
"ai_companies_explanation": "Þú ert að sjá þetta vegna þess að kerfisstjóri þessa vefsvæðis hefur sett upp Anubis til að vernda vefþjóninn fyrir holskeflu beiðna frá svokölluðum gervigreindarfyrirtækjum sem samviskulaust eru að skrapa upplýsingar af vefsvæðum annarra. Þetta getur valdið og veldur töfum og truflunum á þessum vefsvæðum, sem aftur veldur því að efni þeirra verður öllum óaðgengilegt.",
|
||||
"anubis_compromise": "Anubis er millivegur. Anubis notar sönnun-á-vinnu (Proof-of-Work) skema í líkingu við Hashcash, sem er viðlíka skema til að minnka ruslpóst. Hugmyndin er að fyrir almennar heimsóknir verði viðbótarálagið vegna þessa ásættanlegt og valdi litlum truflunum, en við massaskröpun verði samlegðaráhrifin veruleg og geri slíka skröpun upplýsinga of dýra hvað varðar afköst og reiknigetu.",
|
||||
"hack_purpose": "Hafa verður í huga að þetta er \"redding\" sem hefur þann tilgang að gefa \"nógu góða\" bráðabirgðalausn við þessu vandamáli, þannig að hægt sé á meðan að finna betri aðferðir við að auðkenna og gera fingraför til að þekkja viðmótslausa vafra (headless browsers - til dæmis með því hvernig þeir meðhöndla letur) þannig að í framtíðinni þurfi ekki að birta síðuna með sönnun-á-vinnu áskoruninni fyrir notendur sem líklegir eru til að vera löglegir/mennskir notendur.",
|
||||
"jshelter_note": "Athugaðu að Anubis krefst notkunar á ýmsum nútímalegum eiginleikum JavaScript sem viðbætur á borð við JShelter munu gera ávirka. Endilega gerðu JShelter eða álíka viðbætur óvirkar fyrir þetta lén.",
|
||||
"version_info": "Þetta vefsvæði er að keyra Anubis útgáfu",
|
||||
"try_again": "Prófaðu aftur",
|
||||
"go_home": "Farðu aftur heim til þín",
|
||||
"contact_webmaster": "eða ef þú heldur að ekki ætti að loka á þig, þá ættirðu að hafa samband við vefstjórann á",
|
||||
"connection_security": "Hinkraðu augnablik á meðan við tryggjum öryggi tengingarinnar þinnar.",
|
||||
"javascript_required": "Það er leiðinlegt, en þú verður að virkja JavaScript til að komast í gegnum þessa áskorun. Þetta er nauðsynlegt vegna þess að AI-fyrirtækin neita að fara eftir þeim samfélagslegu viðmiðum sem hafa mótað það hvernig vefhýsing virkar. Lausn sem ekki reiðir sig á JS er í vinnslu.",
|
||||
"benchmark_requires_js": "JavaScript þarf að vera virkt til að keyra afkastaprófunarkerfið.",
|
||||
"difficulty": "Erfiðleikastig:",
|
||||
"algorithm": "Reiknirit:",
|
||||
"compare": "Bera saman:",
|
||||
"time": "Tími",
|
||||
"iters": "Umferðir",
|
||||
"time_a": "Tími A",
|
||||
"iters_a": "Umferðir A",
|
||||
"time_b": "Tími B",
|
||||
"iters_b": "Umferðir B",
|
||||
"static_check_endpoint": "Þetta er bara endapunktur prófunar til notkunar fyrir öfuga milliþjóninn (reverse proxy) þinn.",
|
||||
"authorization_required": "Auðkenning nauðsynleg",
|
||||
"cookies_disabled": "Vafrinn þinn er stilltur á að gera vefkökur óvirkar. Anubis þarf að nota vefkökur í þeim tilgangi að tryggja að þú sért með leyfilegt forrit. Vinsamlega virkjaðu vefkökur fyrir þetta lén",
|
||||
"access_denied": "Aðgangi hafnað: villukóði",
|
||||
"dronebl_entry": "DroneBL tilkynnti færslu",
|
||||
"see_dronebl_lookup": "skoðaðu",
|
||||
"internal_server_error": "Innri villa á netþjóni: Kerfisstjóri hefur stillt Anubis rangt. Hafðu samband við kerfisstjóra og biddu þá um að skoða atvikaskrár sem tengjast þessu",
|
||||
"invalid_redirect": "Ógild endurbeining",
|
||||
"redirect_not_parseable": "Slóð endurbeiningar er ekki túlkanleg",
|
||||
"redirect_domain_not_allowed": "Lén endurbeiningar er ekki leyft",
|
||||
"failed_to_sign_jwt": "mistókst að undirrita JWT",
|
||||
"invalid_invocation": "Ógild kvaðning á MakeChallenge",
|
||||
"client_error_browser": "Villa í forriti: Gakktu úr skugga um að vafrinn þinn sé uppfærður í nýjustu útgáfu og prófaðu aftur síðar.",
|
||||
"oh_noes": "Æi nei!",
|
||||
"benchmarking_anubis": "Afkastaprófun Anubis!",
|
||||
"you_are_not_a_bot": "Þú ert ekki botti!",
|
||||
"making_sure_not_bot": "Geng úr skugga um að þú sért ekki botti!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Vafrinn þinn er ekki með web.crypto einindi sem virkar. Ertu að skoða þetta í gegnum öruggt umhverfi?",
|
||||
"js_web_workers_error": "Vafrinn þinn styður ekki vefvaktara (web workers - Anubis notar þetta til að koma í veg fyrir að vafrinn frjósi). Ertu með viðbót á borð við JShelter uppsetta?",
|
||||
"js_cookies_error": "Vafrinn þinn geymir ekki vefkökur. Anubis notar vefkökur til að ákvarða hvaða biðlaraforrit hafi leyst áskoranir og geymir þá undirritað teikn í vefköku. Vinsamlega virkjaðu geymslu á vefkökum fyrir þetta lén. Nöfnin á þeim vefkökum sem Anubis geymir geta breyst fyrirvaralaust. Heiti vefkakna og gildi þeirra eru ekki hluti opinbera API-kerfisviðmótsins.",
|
||||
"js_context_not_secure": "Umhverfið þitt er ekki öruggt!",
|
||||
"js_context_not_secure_msg": "Prófaðu að tengjast í gegnum HTTPS eða láttu kerfisstjórann vita að hann þurfi að setja upp HTTPS. Fyrir nánari upplýsingar er hægt að skoða <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Reikna...",
|
||||
"js_missing_feature": "Eiginleika vantar",
|
||||
"js_challenge_error": "Villa í áskorun!",
|
||||
"js_challenge_error_msg": "Mistókst að leysa reiknirit á prófunar. Þú gætir viljað endurlesa síðuna.",
|
||||
"js_calculating_difficulty": "Reikna...<br/>Erfiðleikastig:",
|
||||
"js_speed": "Hraði:",
|
||||
"js_verification_longer": "Sannvottun tók lengri tíma en búast má við. Ekki endurlesa síðuna.",
|
||||
"js_success": "Tókst!",
|
||||
"js_done_took": "Klárt! Tók",
|
||||
"js_iterations": "umferðir",
|
||||
"js_finished_reading": "Ég hef lokið lestrinum, höldum áfram →",
|
||||
"js_calculation_error": "Reiknivilla!",
|
||||
"js_calculation_error_msg": "Mistókst að reikna áskorun:"
|
||||
}
|
||||