Compare commits

..

10 Commits

Author SHA1 Message Date
Xe Iaso
5756f51495 docs: update CHANGELOG
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-10-17 22:52:31 +00:00
Xe Iaso
b345e0069b fix(algorithms/fast): fix fast challenge on insecure contexts
Closes #1192

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-10-17 22:51:42 +00:00
Xe Iaso
00261d049e fix(default-config): sometimes browsers don't send Upgrade-Insecure-Requests (#1189)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-10-13 18:31:14 +00:00
Thomas Anderson
a12b4bb755 changed redirect_domains docs (#1171) 2025-10-13 16:21:56 +00:00
Xe Iaso
4dfc73abd1 fix(lib): de-flake package lib tests (#1187)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-10-13 11:50:13 -04:00
Xe Iaso
ffbbdce3da feat: default config macro (#1186)
* feat(data): add default-config macro

Closes #1152

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test: add default-config-macro smoke test

This uses an AI generated python script to diff the contents of the bots
field of the default configuration file and the
data/meta/default-config.yaml file. It emits a patch showing what needs
to be changed.

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-10-13 11:33:16 -04:00
Xe Iaso
c09c86778d fix(default-config): remove preact challenge (#1184)
* fix(default-config): remove the preact challenge from the default config

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-10-11 09:22:07 -04:00
Xe Iaso
9c47c180d0 fix(default-config): make the default config far less paranoid (#1179)
* test: add httpdebug tool

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(data/clients/git): more strictly match the git client

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(default-config): make the default config far less paranoid

This uses a variety of heuristics to make sure that clients that claim
to be browsers are more likely to behave like browsers. Most of these
are based on the results of a lot of reverse engineering and data
collection from honeypot servers.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>
2025-10-11 08:48:12 -04:00
Xe Iaso
d51d32726c fix(lib): serve CSS properly (#1158)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-27 22:16:23 -04:00
dependabot[bot]
ff33982ee9 build(deps): bump github.com/docker/docker (#1131)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.3.2+incompatible to 28.3.3+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.2...v28.3.3)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.3.3+incompatible
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-27 14:29:07 -04:00
74 changed files with 532 additions and 2751 deletions

View File

@@ -4,7 +4,7 @@ WORKDIR /app
COPY go.mod go.sum package.json package-lock.json ./
RUN apt-get update \
&& apt-get -y install zstd brotli redis uuid-runtime binaryen \
&& apt-get -y install zstd brotli redis \
&& mkdir -p /home/vscode/.local/share/fish \
&& chown -R vscode:vscode /home/vscode/.local/share/fish \
&& chown -R vscode:vscode /go

View File

@@ -10,11 +10,7 @@
"postStartCommand": "bash ./.devcontainer/poststart.sh",
"features": {
"ghcr.io/xe/devcontainer-features/ko:1.1.0": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/rust:1": {
"version": "latest",
"targets": "wasm32-unknown-unknown"
}
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"initializeCommand": "mkdir -p ${localEnv:HOME}${localEnv:USERPROFILE}/.local/share/atuin",
"customizations": {

View File

@@ -5,6 +5,5 @@ pwd
npm ci &
go mod download &
go install ./utils/cmd/... &
cargo fetch &
wait

View File

@@ -22,7 +22,6 @@ bbolt
bdba
berr
bezier
binaryen
bingbot
Bitcoin
bitrate
@@ -42,7 +41,6 @@ cachediptoasn
Caddyfile
caninetools
Cardyb
cdylib
celchecker
celphase
cerr
@@ -59,9 +57,7 @@ checkresult
chibi
cidranger
ckie
clippy
cloudflare
codegen
Codespaces
confd
connnection
@@ -73,15 +69,11 @@ crt
Cscript
daemonizing
dayjob
dce
DDOS
dealign
Debian
debrpm
decaymap
denan
devcontainers
dfo
Diffbot
discordapp
discordbot
@@ -100,7 +92,6 @@ eerror
ellenjoe
emacs
enbyware
equix
etld
everyones
evilbot
@@ -113,18 +104,14 @@ facebookgo
Factset
fastcgi
fediverse
fff
ffm
ffprobe
financials
finfos
Firecrawl
flagenv
fnames
Fordola
forgejo
forwardauth
fpcast
fsys
fullchain
gaissmai
@@ -152,7 +139,6 @@ grpcprom
grw
Hashcash
hashrate
hashx
headermap
healthcheck
healthz
@@ -161,7 +147,6 @@ Hetzner
hmc
homelab
hostable
hostimport
htmlc
htmx
httpdebug
@@ -172,7 +157,6 @@ iaskspider
iaso
iat
ifm
iit
Imagesift
imgproxy
impressum
@@ -184,7 +168,6 @@ iptoasn
isp
iss
isset
iterand
ivh
Jenomis
JGit
@@ -198,7 +181,6 @@ kagibot
Keyfunc
keypair
KHTML
kilohashes
kinda
KUBECONFIG
lcj
@@ -213,11 +195,9 @@ limsa
Linting
linuxbrew
LLU
lmu
loadbalancer
lol
lominsa
lto
maintainership
malware
mcr
@@ -284,16 +264,12 @@ redir
redirectscheme
refactors
reputational
rereloop
risc
rse
ruleset
runlevels
RUnlock
runtimedir
runtimedirectory
RUSTFLAGS
rustup
Ryzen
sas
sasl
@@ -309,10 +285,10 @@ shellcheck
shirou
shopt
Sidetrade
simd
simprint
sitemap
sls
Smartphone
sni
Spambot
sparkline
@@ -337,25 +313,20 @@ techarohq
templ
templruntime
testarea
tetratelabs
Thancred
thoth
thothmock
Tik
Timpibot
TLog
tnh
traefik
trunc
uberspace
Unbreak
unbreakdocker
unifiedjs
uninlined
unmarshal
unparseable
untee
usize
uvx
uwu
UXP
@@ -366,8 +337,6 @@ vendored
vhosts
VKE
Vultr
wasmjs
wazero
weblate
webmaster
webpage

View File

@@ -2,7 +2,7 @@ name: Docker image builds (pull requests)
on:
pull_request:
branches: ["main"]
branches: [ "main" ]
env:
DOCKER_METADATA_SET_OUTPUT_ENV: "true"
@@ -45,17 +45,6 @@ jobs:
run: |
brew bundle
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- name: Setup Binaryen
uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0

View File

@@ -55,17 +55,6 @@ jobs:
run: |
brew bundle
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- name: Setup Binaryen
uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- name: Log into registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:

View File

@@ -2,9 +2,9 @@ name: Go
on:
push:
branches: ["main"]
branches: [ "main" ]
pull_request:
branches: ["main"]
branches: [ "main" ]
permissions:
contents: read
@@ -15,88 +15,77 @@ jobs:
#runs-on: alrest-techarohq
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- 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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-golang-
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- name: Cache playwright binaries
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
- name: Setup Binaryen
uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- name: install node deps
run: |
npm ci
- name: Cache playwright binaries
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
- name: install playwright browsers
run: |
npx --no-install playwright@1.52.0 install --with-deps
npx --no-install playwright@1.52.0 run-server --port 9001 &
- name: install node deps
run: |
npm ci
- name: Build
run: npm run build
- name: install playwright browsers
run: |
npx --no-install playwright@1.52.0 install --with-deps
npx --no-install playwright@1.52.0 run-server --port 9001 &
- name: Test
run: npm run test
- name: Build
run: npm run build
- name: Lint with staticcheck
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0
with:
version: "latest"
- name: Test
run: npm run test
- name: Lint with staticcheck
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0
with:
version: "latest"
- name: Govulncheck
run: |
go tool govulncheck ./...
- name: Govulncheck
run: |
go tool govulncheck ./...

View File

@@ -59,17 +59,6 @@ jobs:
restore-keys: |
${{ runner.os }}-golang-
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- name: Setup Binaryen
uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- name: install node deps
run: |
npm ci

View File

@@ -2,9 +2,9 @@ name: Package builds (unstable)
on:
push:
branches: ["main"]
branches: [ "main" ]
pull_request:
branches: ["main"]
branches: [ "main" ]
permissions:
contents: read
@@ -15,71 +15,60 @@ jobs:
#runs-on: alrest-techarohq
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
fetch-tags: true
fetch-depth: 0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-golang-
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- name: install node deps
run: |
npm ci
- name: Setup Binaryen
uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- name: Build Packages
run: |
go tool yeet
- name: install node deps
run: |
npm ci
- name: Build Packages
run: |
go tool yeet
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: packages
path: var/*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: packages
path: var/*

View File

@@ -1,35 +0,0 @@
name: "Rust tests"
on:
push:
pull_request:
jobs:
rust-test:
name: cargo test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- run: cargo test --all-features
# Check formatting with rustfmt
rust-formatting:
name: cargo fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
# Ensure rustfmt is installed and setup problem matcher
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
components: rustfmt
cache: false
target: wasm32-unknown-unknown
- name: Rustfmt Check
uses: actions-rust-lang/rustfmt@559aa3035a47390ba96088dffa783b5d26da9326 # v1.1.1

View File

@@ -14,6 +14,7 @@ jobs:
strategy:
matrix:
test:
- default-config-macro
- double_slash
- forced-language
- git-clone
@@ -37,11 +38,6 @@ jobs:
with:
go-version: stable
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- name: Install utils

View File

@@ -0,0 +1,37 @@
name: Regenerate ssh ci runner image
on:
# pull_request:
# branches: ["main"]
schedule:
- cron: "0 0 1,8,15,22 * *"
workflow_dispatch:
permissions:
pull-requests: write
contents: write
packages: write
jobs:
ssh-ci-rebuild:
if: github.repository == 'TecharoHQ/anubis'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Log into registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push
run: |
cd ./test/ssh-ci
docker buildx bake --push

View File

@@ -3,8 +3,8 @@ name: SSH CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
# pull_request:
# branches: ["main"]
permissions:
contents: read
@@ -39,17 +39,6 @@ jobs:
with:
go-version: stable
- uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
with:
cache: false
target: wasm32-unknown-unknown
- name: Setup Binaryen
uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- name: Run CI
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
env:

6
.gitignore vendored
View File

@@ -21,8 +21,4 @@ node_modules
# how does this get here
doc/VERSION
web/static/locales/*.json
# Rust
target/*
*.wasm
web/static/locales/*.json

431
Cargo.lock generated
View File

@@ -1,431 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anubis"
version = "0.1.0"
dependencies = [
"wee_alloc",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.11.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9ef36a6fcdb072aa548f3da057640ec10859eb4e91ddf526ee648d50c76a949"
dependencies = [
"hybrid-array",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "const-oid"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.0-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6"
dependencies = [
"hybrid-array",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common 0.1.6",
"subtle",
]
[[package]]
name = "digest"
version = "0.11.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6749b668519cd7149ee3d11286a442a8a8bdc3a9d529605f579777bfccc5a4bc"
dependencies = [
"block-buffer 0.11.0-rc.5",
"const-oid",
"crypto-common 0.2.0-rc.4",
]
[[package]]
name = "dynasm"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625"
dependencies = [
"bitflags",
"byteorder",
"lazy_static",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dynasmrt"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55"
dependencies = [
"byteorder",
"dynasm",
"fnv",
"memmap2",
]
[[package]]
name = "equix"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b48d834668afc862e90cda69065c0d85c192fe0a9364d1c5c05baf21c7454fc9"
dependencies = [
"arrayvec",
"hashx 0.4.0",
"num-traits",
"thiserror",
"visibility",
]
[[package]]
name = "fixed-capacity-vec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b31a14f5ee08ed1a40e1252b35af18bed062e3f39b69aab34decde36bc43e40"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hashx"
version = "0.1.0"
dependencies = [
"anubis",
"equix",
"hashx 0.4.0",
]
[[package]]
name = "hashx"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8af708f5ff697572d409331c8714984a0ec1bb2eac190dd5bd75785bad6e764"
dependencies = [
"arrayvec",
"blake2",
"dynasmrt",
"fixed-capacity-vec",
"hex",
"rand_core",
"thiserror",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hybrid-array"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a09fa0190457fce307a699c050054974f81b6975b7a017f1e784eb7d9c2d4bc"
dependencies = [
"typenum",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "memmap2"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7"
dependencies = [
"libc",
]
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
[[package]]
name = "sha2"
version = "0.11.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924"
dependencies = [
"cfg-if 1.0.3",
"cpufeatures",
"digest 0.11.0-rc.2",
]
[[package]]
name = "sha256"
version = "0.1.0"
dependencies = [
"anubis",
"sha2",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "visibility"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@@ -1,10 +0,0 @@
[workspace]
resolver = "2"
members = ["wasm/anubis", "wasm/pow/*"]
[profile.release]
#strip = true
opt-level = "s"
lto = "thin"
codegen-units = 1
panic = "abort"

View File

@@ -1,22 +1,17 @@
VERSION= $(shell cat ./VERSION)
GO?= go
NPM?= npm
CARGO?= cargo
.PHONY: build assets assets-wasm deps lint prebaked-build test
.PHONY: build assets deps lint prebaked-build test
all: build
deps:
$(NPM) ci
$(GO) mod download
$(CARGO) fetch
assets-wasm:
bash ./scripts/build_wasm.sh
assets: PATH:=$(PWD)/node_modules/.bin:$(PATH)
assets: deps assets-wasm
assets: deps
$(GO) generate ./...
./web/build.sh
./xess/build.sh

View File

@@ -11,6 +11,9 @@
## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
bots:
# You can import the entire default config with this macro:
# - import: (data)/meta/default-config.yaml
# Pathological bots to deny
- # This correlates to data/bots/_deny-pathological.yaml in the source tree
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/_deny-pathological.yaml
@@ -93,6 +96,50 @@ bots:
# weight:
# adjust: -10
# Assert behaviour that only genuine browsers display. This ensures that Chrome
# or Firefox versions
- name: realistic-browser-catchall
expression:
all:
- '"User-Agent" in headers'
- '( userAgent.contains("Firefox") ) || ( userAgent.contains("Chrome") ) || ( userAgent.contains("Safari") )'
- '"Accept" in headers'
- '"Sec-Fetch-Dest" in headers'
- '"Sec-Fetch-Mode" in headers'
- '"Sec-Fetch-Site" in headers'
- '"Accept-Encoding" in headers'
- '( headers["Accept-Encoding"].contains("zstd") || headers["Accept-Encoding"].contains("br") )'
- '"Accept-Language" in headers'
action: WEIGH
weight:
adjust: -10
# The Upgrade-Insecure-Requests header is typically sent by browsers, but not always
- name: upgrade-insecure-requests
expression: '"Upgrade-Insecure-Requests" in headers'
action: WEIGH
weight:
adjust: -2
# Chrome should behave like Chrome
- name: chrome-is-proper
expression:
all:
- userAgent.contains("Chrome")
- '"Sec-Ch-Ua" in headers'
- 'headers["Sec-Ch-Ua"].contains("Chromium")'
- '"Sec-Ch-Ua-Mobile" in headers'
- '"Sec-Ch-Ua-Platform" in headers'
action: WEIGH
weight:
adjust: -5
- name: should-have-accept
expression: '!("Accept" in headers)'
action: WEIGH
weight:
adjust: 5
# Generic catchall rule
- name: generic-browser
user_agent_regex: >-
@@ -212,14 +259,10 @@ thresholds:
- weight < 20
action: CHALLENGE
challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/preact
#
# This challenge proves the client can run a webapp written with Preact.
# The preact webapp simply loads, calculates the SHA-256 checksum of the
# challenge data, and forwards that to the client.
algorithm: preact
difficulty: 1
report_as: 1
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 2 # two leading zeros, very fast for most clients
report_as: 2
- name: mild-proof-of-work
expression:
all:
@@ -229,8 +272,8 @@ thresholds:
challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 2 # two leading zeros, very fast for most clients
report_as: 2
difficulty: 4
report_as: 4
# For clients that are browser like and have gained many points from custom rules
- name: extreme-suspicion
expression: weight >= 30
@@ -238,5 +281,5 @@ thresholds:
challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 4
report_as: 4
difficulty: 6
report_as: 6

View File

@@ -2,13 +2,19 @@
action: ALLOW
expression:
all:
- >
(
userAgent.startsWith("git/") ||
userAgent.contains("libgit") ||
userAgent.startsWith("go-git") ||
userAgent.startsWith("JGit/") ||
userAgent.startsWith("JGit-")
)
- '"Git-Protocol" in headers'
- headers["Git-Protocol"] == "version=2"
- >
(
userAgent.startsWith("git/") ||
userAgent.contains("libgit") ||
userAgent.startsWith("go-git") ||
userAgent.startsWith("JGit/") ||
userAgent.startsWith("JGit-")
)
- '"Accept" in headers'
- headers["Accept"] == "*/*"
- '"Cache-Control" in headers'
- headers["Cache-Control"] == "no-cache"
- '"Pragma" in headers'
- headers["Pragma"] == "no-cache"
- '"Accept-Encoding" in headers'
- headers["Accept-Encoding"].contains("gzip")

View File

@@ -0,0 +1,133 @@
- # Pathological bots to deny
# This correlates to data/bots/_deny-pathological.yaml in the source tree
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/_deny-pathological.yaml
import: (data)/bots/_deny-pathological.yaml
- import: (data)/bots/aggressive-brazilian-scrapers.yaml
# Aggressively block AI/LLM related bots/agents by default
- import: (data)/meta/ai-block-aggressive.yaml
# Consider replacing the aggressive AI policy with more selective policies:
# - import: (data)/meta/ai-block-moderate.yaml
# - import: (data)/meta/ai-block-permissive.yaml
# Search engine crawlers to allow, defaults to:
# - Google (so they don't try to bypass Anubis)
# - Apple
# - Bing
# - DuckDuckGo
# - Qwant
# - The Internet Archive
# - Kagi
# - Marginalia
# - Mojeek
- import: (data)/crawlers/_allow-good.yaml
# Challenge Firefox AI previews
- import: (data)/clients/x-firefox-ai.yaml
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
- import: (data)/common/keep-internet-working.yaml
# # Punish any bot with "bot" in the user-agent string
# # This is known to have a high false-positive rate, use at your own risk
# - name: generic-bot-catchall
# user_agent_regex: (?i:bot|crawler)
# action: CHALLENGE
# challenge:
# difficulty: 16 # impossible
# report_as: 4 # lie to the operator
# algorithm: slow # intentionally waste CPU cycles and time
# Requires a subscription to Thoth to use, see
# https://anubis.techaro.lol/docs/admin/thoth#geoip-based-filtering
- name: countries-with-aggressive-scrapers
action: WEIGH
geoip:
countries:
- BR
- CN
weight:
adjust: 10
# Requires a subscription to Thoth to use, see
# https://anubis.techaro.lol/docs/admin/thoth#asn-based-filtering
- name: aggressive-asns-without-functional-abuse-contact
action: WEIGH
asns:
match:
- 13335 # Cloudflare
- 136907 # Huawei Cloud
- 45102 # Alibaba Cloud
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
# Assert behaviour that only genuine browsers display. This ensures that Chrome
# or Firefox versions
- name: realistic-browser-catchall
expression:
all:
- '"User-Agent" in headers'
- '( userAgent.contains("Firefox") ) || ( userAgent.contains("Chrome") ) || ( userAgent.contains("Safari") )'
- '"Accept" in headers'
- '"Sec-Fetch-Dest" in headers'
- '"Sec-Fetch-Mode" in headers'
- '"Sec-Fetch-Site" in headers'
- '"Accept-Encoding" in headers'
- '( headers["Accept-Encoding"].contains("zstd") || headers["Accept-Encoding"].contains("br") )'
- '"Accept-Language" in headers'
action: WEIGH
weight:
adjust: -10
# The Upgrade-Insecure-Requests header is typically sent by browsers, but not always
- name: upgrade-insecure-requests
expression: '"Upgrade-Insecure-Requests" in headers'
action: WEIGH
weight:
adjust: -2
# Chrome should behave like Chrome
- name: chrome-is-proper
expression:
all:
- userAgent.contains("Chrome")
- '"Sec-Ch-Ua" in headers'
- 'headers["Sec-Ch-Ua"].contains("Chromium")'
- '"Sec-Ch-Ua-Mobile" in headers'
- '"Sec-Ch-Ua-Platform" in headers'
action: WEIGH
weight:
adjust: -5
- name: should-have-accept
expression: '!("Accept" in headers)'
action: WEIGH
weight:
adjust: 5
# Generic catchall rule
- name: generic-browser
user_agent_regex: >-
Mozilla|Opera
action: WEIGH
weight:
adjust: 10

View File

@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- This changes the project to: -->
- Added `(data)/meta/default-config.yaml` for importing the entire default configuration at once.
- Add `-custom-real-ip-header` flag to get the original request IP from a different header than `x-real-ip`.
- Add `contentLength` variable to bot expressions.
- Add `COOKIE_SAME_SITE_MODE` to force anubis cookies SameSite value, and downgrade automatically from `None` to `Lax` if cookie is insecure.
@@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086)).
- Add validation warning when persistent storage is used without setting signing keys.
- Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925)).
- Make the `fast` algorithm prefer purejs when running in an insecure context.
- Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend.
- Fix a "stutter" in the cookie name prefix so the auth cookie is named `techaro.lol-anubis-auth` instead of `techaro.lol-anubis-auth-auth`.
- Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines.
@@ -29,25 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixes concurrency problems with very old browsers ([#1082](https://github.com/TecharoHQ/anubis/issues/1082)).
- Randomly use the Refresh header instead of the meta refresh tag in the metarefresh challenge.
- Update OpenRC service to truncate the runtime directory before starting Anubis.
- Make the git client profile more strictly match how the git client behaves.
- Make the default configuration reward users using normal browsers.
- Allow multiple consecutive slashes in a row in application paths ([#754](https://github.com/TecharoHQ/anubis/issues/754)).
- Add option to set `targetSNI` to special keyword 'auto' to indicate that it should be automatically set to the request Host name ([424](https://github.com/TecharoHQ/anubis/issues/424)).
### Major new features
#### WebAssembly support
Anubis now supports running [WebAssembly based proof of work challenges](./admin/configuration/challenges/wasm.mdx) in addition to pure-JavaScript challenges. For more information, check the following links:
- [Proof of Work (WebAssembly)](./admin/configuration/challenges/wasm.mdx)
- [WebAssembly based proof of work implementation details](./developer/wasm.mdx)
:::note
Clients that don't have WebAssembly enabled will instead be served a pure JavaScript variant of the WebAssembly module. This will be much slower than the WebAssembly module, but will work.
:::
If you are packaging this for a Linux distribution, Anubis requires [binaryen](https://github.com/WebAssembly/binaryen) at exactly version 108. We are working on mitigating this dependency.
- The Preact challenge has been removed from the default configuration. It will be deprecated in the future.
### Bug Fixes

View File

@@ -4,7 +4,6 @@ Anubis supports multiple challenge methods:
- [Meta Refresh](./metarefresh.mdx)
- [Preact](./preact.mdx)
- [Proof of Work (JS)](./proof-of-work.mdx)
- [Proof of Work (WebAssembly)](./wasm.mdx)
- [Proof of Work](./proof-of-work.mdx)
Read the documentation to know which method is best for you.

View File

@@ -1,66 +0,0 @@
# Proof of Work (WebAssembly)
Anubis supports using [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly) to speed up proof of work validation. Proof of work functions as a randomized delay to prevent clients from overwhelming your server with traffic. All WebAssembly proof of work functions are written in Rust. If clients are running in a context without WebAssembly support, Anubis uses a variant of the WebAssembly code compiled to JavaScript.
Anubis offers the following WebAssembly proof of work functions:
- [hashx](#hashx) (via the [`hashx`](https://docs.rs/hashx/latest/hashx/index.html) crate)
- [sha256](#sha256) (via the [`sha2`](https://docs.rs/sha2/latest/sha2/) crate)
:::note
The difficulty values for the WebAssembly based checks are going to be much higher than the equivalent difficulty values for the [JavaScript based checks](./proof-of-work.mdx). Generally these count the number of leading _bits_ that much match instead of the number of leading _nibbles_ that must match. Here's a rough translation table:
| `fast` difficulty | `sha256` difficulty | `hashx` difficulty |
| :---------------- | :------------------ | :----------------- |
| `4` | `16` | `15` |
| `2` | `8` | `7` |
| `6` | `24` | `20` |
:::
## `hashx`
Uses the ASIC-resistant [hashx](https://github.com/tevador/hashx) as the proof of work function. This is resistant to GPU and ASIC attacks. In practice this means that users need to be using a standards-compliant browser to pass the challenge.
Usage:
```yaml
thresholds:
- name: moderate-suspicion
expression:
all:
- weight >= 10
- weight < 20
action: CHALLENGE
challenge:
algorithm: hashx
difficulty: 16
report_as: 16
```
## `sha256`
Uses [SHA-256](https://en.wikipedia.org/wiki/SHA-2) as the proof of work function.
:::note
This is included mostly as a fallback for usecases that specifically require this hashing function. There are known tools that solve this particular challenge. Prefer [hashx](#hashx) unless you know what you are doing.
:::
Usage:
```yaml
thresholds:
- name: moderate-suspicion
expression:
all:
- weight >= 10
- weight < 20
action: CHALLENGE
challenge:
algorithm: sha256
difficulty: 16
report_as: 16
```

View File

@@ -32,7 +32,7 @@ sequenceDiagram
participant Validation
participant Evil Site
Hacker->>+User: Click on yoursite.com with this solution
Hacker->>+User: Click on example.org with this solution
User->>+Validation: Here's a solution, send me to evilsite.com
Validation->>+User: Here's a cookie, go to evilsite.com
User->>+Evil Site: GET evilsite.com
@@ -46,11 +46,14 @@ Redirect domain not allowed
## Configuring allowed redirect domains
By default, Anubis will limit redirects to be on the same HTTP Host that Anubis is running on (EG: requests to yoursite.com cannot redirect outside of yoursite.com). If you need to set more than one domain, fill the `REDIRECT_DOMAINS` environment variable with a comma-separated list of domain names that Anubis should allow redirects to.
By default, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.
One can restrict the domains that Anubis can redirect to when passing a challenge by setting up `REDIRECT_DOMAINS` environment variable.
If you need to set more than one domain, fill the environment variable with a comma-separated list of domain names.
There is also glob matching support. You can pass `*.bugs.techaro.lol` to allow redirecting to anything ending with `.bugs.techaro.lol`. There is a limit of 4 wildcards.
:::note
These domains are _an exact string match_, they do not support wildcard matches.
If you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here.
:::
@@ -60,7 +63,7 @@ These domains are _an exact string match_, they do not support wildcard matches.
```shell
# anubis.env
REDIRECT_DOMAINS="yoursite.com,secretplans.yoursite.com"
REDIRECT_DOMAINS="example.org,secretplans.example.org,*.test.example.org"
# ...
```
@@ -72,7 +75,7 @@ services:
anubis-nginx:
image: ghcr.io/techarohq/anubis:latest
environment:
REDIRECT_DOMAINS: "yoursite.com,secretplans.yoursite.com"
REDIRECT_DOMAINS: "example.org,secretplans.example.org,*.test.example.org"
# ...
```
@@ -86,7 +89,7 @@ Inside your Deployment, StatefulSet, or Pod:
image: ghcr.io/techarohq/anubis:latest
env:
- name: REDIRECT_DOMAINS
value: "yoursite.com,secretplans.yoursite.com"
value: "example.org,secretplans.example.org,*.test.example.org"
# ...
```

View File

@@ -95,7 +95,7 @@ Anubis uses these environment variables for configuration:
| `OVERLAY_FOLDER` | unset | <EO /> If set, treat the given path as an [overlay folder](./botstopper.mdx#custom-images-and-css), allowing you to customize CSS, fonts, images, and add other assets to BotStopper deployments. |
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
| `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). |
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.<br/><br/>Note that if you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here. |
| `REDIRECT_DOMAINS` | unset | Comma-separated list of domain names that Anubis should allow redirects to when passing a challenge. See [Redirect Domain Configuration](./configuration/redirect-domains) for more details. |
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
| `SLOG_LEVEL` | `INFO` | The log level for structured logging. Valid values are `DEBUG`, `INFO`, `WARN`, and `ERROR`. Set to `DEBUG` to see all requests, evaluations, and detailed diagnostic information. |
| `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. |

View File

@@ -1,75 +0,0 @@
# WebAssembly based proof of work implementation details
When an administrator configures Anubis to use a [WebAssembly based proof of work check](../admin/configuration/challenges/wasm.mdx), Anubis serves [WebAssembly modules](https://webassembly.github.io/spec/core/syntax/modules.html) to clients. Clients execute these WebAssembly modules in order to solve challenges and the server executes the same WebAssembly modules in order to validate solutions. This architecture allows the client and server to be implemented in lockstep, making it much easier to add future challenge methods as long as they meet the API contract.
## Design goals
Anubis WebAssembly modules are meant to provide the following properties:
- **Small size** - Modules should not be bigger than 128Ki unless there is a very good reason for them to be larger.
- **Minimal interfaces to the outside world** - These are meant to run hash functions. Minimize the inputs and outputs to the bare minimum required to run the program.
- **The same code must run on both the client and server** - In order to simplify implementation, expansion, and technical debt: the same code must run on both the client and the server. When a client computes a solution, it uses the same validation code that the server uses to verify correctness.
- **Execution as fast as possible** - This code is in a unique deployment scenario. At some level you want the code to execute slowly to better function as a rate limiter for incoming client requests. At another level, you want the code to execute as quickly as possible so that clients don't have a bad experience with the check taking "too long" or causing battery life impacts.
Given these constraints, the following compromises are made:
- WebAssembly modules are written in [Rust](https://rust-lang.org/), a modern systems programming language with best-in-class support for WebAssembly.
- [Wazero](https://wazero.io/) is used on the server for running the validation logic in a secure sandbox.
- A low-level Unix-like API is used to communicate between the host and the guest.
## API contract
Anubis WebAssembly modules have two main entrypoints:
- `anubis_work`: Reads the data buffer and works until the validation function says that the solution is correct.
- `anubis_validate`: Reads the verification and data buffers and ensures that both of them match and the solution is correct.
For an example of an Anubis WebAssembly module, read the source code for the [`sha256` challenge](https://github.com/TecharoHQ/anubis/blob/main/wasm/pow/sha256/src/lib.rs).
Anubis WebAssembly modules have the following de-facto global variables:
- The data buffer: where the challenge-specific random data is stored and used for computing challenge results. Limit of 4096 bytes.
- The result buffer: where the result hash is stored. Limit varies based on challenge.
- The verification buffer: where a verification hash is written to for comparison with the computed hash in the result buffer. Limit varies based on challenge.
Other functions:
### Writing to the data buffer
The data buffer is a write-only buffer with a maximum capacity of 4096 bytes. The host writes data into the buffer and sets the buffer length. This functions similarly to a Go slice.
Usage:
- Host: call `data_ptr` to discover the base pointer of the data buffer.
- Guest: return the pointer.
- Host: write up to 4096 bytes to guest memory starting at the base pointer.
- Host: call `set_data_length` to tell the guest how large the data buffer is.
- Guest: update that length as a global mutable variable.
- Host: call `anubis_work` or set the verification buffer and call `anubis_validate`.
### Reading from the result buffer
The result buffer is a read-only buffer that contains the result computed by `anubis_work`. The size of this buffer varies based on the challenge implementation.
After calling `anubis_work`:
- Host: call `result_hash_ptr` to discover the base pointer of the result buffer.
- Guest: return the pointer.
- Host: call `result_hash_size` to discover the size of the result buffer in bytes.
- Guest: return the size.
- Host: read exactly that number of bytes to host memory.
- Host: use that result to continue on with normal execution.
### Writing to the verification buffer
The verification buffer is a write-only buffer that contains the result computed by a client. The size of this buffer varies based on the challenge implementation.
After setting the data buffer:
- Host: call `verification_hash_ptr` to discover the base pointer of the verification buffer.
- Guest: return that pointer.
- Host: call `verification_hash_size` to discover the size of the verification buffer in bytes.
- Guest: return the size.
- Host: write exactly that number of bytes to guest memory starting at the base pointer.
- Host: call `anubis_validate` with settings from the server-side challenge information.
- Guest: compute both hashes, compare them and validate against the server-side challenge level. If valid: return true. If invalid: return false.

3
go.mod
View File

@@ -25,7 +25,6 @@ require (
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
github.com/tetratelabs/wazero v1.9.0
go.etcd.io/bbolt v1.4.2
golang.org/x/net v0.42.0
golang.org/x/text v0.27.0
@@ -88,7 +87,7 @@ 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.5 // indirect
github.com/docker/docker v28.3.2+incompatible // indirect
github.com/docker/docker v28.3.3+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-20250630131328-58d95d85e994 // indirect

6
go.sum
View File

@@ -141,8 +141,8 @@ 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.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+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=
@@ -395,8 +395,6 @@ github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 h1:YGHgrVjGTYHY98II6zijXUH
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4/go.mod h1:sSi6xaUaHfaqu32ECLeyE7NTMv+ZM5dW0JikhllaalY=
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/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=

View File

@@ -37,7 +37,6 @@ import (
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
_ "github.com/TecharoHQ/anubis/lib/challenge/preact"
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
_ "github.com/TecharoHQ/anubis/lib/challenge/wasm"
)
var (

View File

@@ -194,6 +194,7 @@ func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, er
// Only set if not already present
req = req.Clone(req.Context()) // avoid mutating original request
req.Header.Set("User-Agent", "Mozilla/5.0")
req.Header.Set("Accept-Encoding", "gzip")
return u.rt.RoundTrip(req)
}

View File

@@ -58,7 +58,7 @@ type ValidateInput struct {
type Impl interface {
// Setup registers any additional routes with the Impl for assets or API routes.
Setup(mux *http.ServeMux) error
Setup(mux *http.ServeMux)
// Issue a new challenge to the user, called by the Anubis.
Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)

View File

@@ -21,7 +21,7 @@ func init() {
type Impl struct{}
func (i *Impl) Setup(mux *http.ServeMux) error { return nil }
func (i *Impl) Setup(mux *http.ServeMux) {}
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) {
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")

View File

@@ -1,6 +1,6 @@
import { render, h, Fragment } from "preact";
import { useState, useEffect } from "preact/hooks";
import { g, j, r, u, x } from "../../../../web/lib/xeact";
import { g, j, r, u, x } from "./xeact.js";
import { Sha256 } from "@aws-crypto/sha256-js";
/** @jsx h */

View File

@@ -36,7 +36,7 @@ func init() {
type impl struct{}
func (i *impl) Setup(mux *http.ServeMux) error { return nil }
func (i *impl) Setup(mux *http.ServeMux) {}
func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) {
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")

View File

@@ -25,7 +25,7 @@ type Impl struct {
Algorithm string
}
func (i *Impl) Setup(mux *http.ServeMux) error { return nil }
func (i *Impl) Setup(mux *http.ServeMux) {}
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
loc := localization.GetLocalizer(r)

View File

@@ -1,102 +0,0 @@
package wasm
import (
"context"
"encoding/hex"
"fmt"
"log/slog"
"net/http"
"strconv"
chall "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization"
"github.com/TecharoHQ/anubis/wasm"
"github.com/TecharoHQ/anubis/web"
"github.com/a-h/templ"
)
//go:generate go tool github.com/a-h/templ/cmd/templ generate
func init() {
chall.Register("hashx", &Impl{algorithm: "hashx"})
chall.Register("sha256", &Impl{algorithm: "sha256"})
}
type Impl struct {
algorithm string
runner *wasm.Runner
}
func (i *Impl) Setup(mux *http.ServeMux) error {
fname := fmt.Sprintf("static/wasm/simd128/%s.wasm", i.algorithm)
fin, err := web.Static.Open(fname)
if err != nil {
return err
}
defer fin.Close()
i.runner, err = wasm.NewRunner(context.Background(), fname, fin)
if err != nil {
return err
}
return nil
}
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
loc := localization.GetLocalizer(r)
return page(loc), nil
}
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
nonceStr := r.FormValue("nonce")
if nonceStr == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
}
nonce, err := strconv.Atoi(nonceStr)
if err != nil {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
}
elapsedTimeStr := r.FormValue("elapsedTime")
if elapsedTimeStr == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w elapsedTime", chall.ErrMissingField))
}
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
if err != nil {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: elapsedTime: %w", chall.ErrInvalidFormat, err))
}
response := r.FormValue("response")
if response == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
}
challengeBytes, err := hex.DecodeString(in.Challenge.RandomData)
if err != nil {
return chall.NewError("validate", "invalid random data", fmt.Errorf("can't decode random data: %w", err))
}
gotBytes, err := hex.DecodeString(response)
if err != nil {
return chall.NewError("validate", "invalid client data format", fmt.Errorf("%w response", chall.ErrInvalidFormat))
}
ok, err := i.runner.Verify(r.Context(), challengeBytes, gotBytes, uint32(nonce), uint32(in.Rule.Challenge.Difficulty))
if err != nil {
return chall.NewError("validate", "internal WASM error", fmt.Errorf("can't run wasm validation logic: %w", err))
}
if !ok {
return chall.NewError("verify", "client calculated wrong data", fmt.Errorf("%w: response invalid: %s", chall.ErrFailed, response))
}
lg.Debug("challenge took", "elapsedTime", elapsedTime)
chall.TimeTaken.WithLabelValues(i.algorithm).Observe(elapsedTime)
return nil
}

View File

@@ -1,44 +0,0 @@
package wasm
import (
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/lib/localization"
)
templ page(localizer *localization.SimpleLocalizer) {
<div class="centered-div">
<img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
<p id="status">{ localizer.T("loading") }</p>
<script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
<div id="progress" role="progressbar" aria-labelledby="status">
<div class="bar-inner"></div>
</div>
<details>
if anubis.UseSimplifiedExplanation {
<p>
{ localizer.T("simplified_explanation") }
</p>
} else {
<p>
{ localizer.T("ai_companies_explanation") }
</p>
<p>
{ localizer.T("anubis_compromise") }
</p>
<p>
{ localizer.T("hack_purpose") }
</p>
<p>
{ localizer.T("jshelter_note") }
</p>
}
</details>
<noscript>
<p>
{ localizer.T("javascript_required") }
</p>
</noscript>
<div id="testarea"></div>
</div>
}

View File

@@ -1,190 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924
package wasm
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/lib/localization"
)
func page(localizer *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 10, Col: 165}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"> <img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 11, Col: 174}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p id=\"status\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 12, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><script async type=\"module\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 13, Col: 136}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"></script><div id=\"progress\" role=\"progressbar\" aria-labelledby=\"status\"><div class=\"bar-inner\"></div></div><details>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if anubis.UseSimplifiedExplanation {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("simplified_explanation"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 20, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 24, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 27, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 30, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 33, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</details><noscript><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("javascript_required"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 39, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></noscript><div id=\"testarea\"></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -175,9 +175,7 @@ func New(opts Options) (*Server, error) {
for _, implKind := range challenge.Methods() {
impl, _ := challenge.Get(implKind)
if err := impl.Setup(mux); err != nil {
return nil, fmt.Errorf("failed to init challenge method %s: %w", implKind, err)
}
impl.Setup(mux)
}
result.mux = mux

View File

@@ -17,6 +17,7 @@ import (
"github.com/TecharoHQ/anubis/lib/localization"
"github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/web"
"github.com/TecharoHQ/anubis/xess"
"github.com/a-h/templ"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/net/publicsuffix"
@@ -282,6 +283,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+anubis.StaticPath) {
s.mux.ServeHTTP(w, r)
return
} else if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+xess.BasePrefix) {
s.mux.ServeHTTP(w, r)
return
}
s.maybeReverseProxyOrPage(w, r)

16
package-lock.json generated
View File

@@ -10,9 +10,7 @@
"license": "ISC",
"dependencies": {
"@aws-crypto/sha256-js": "^5.2.0",
"@haribala/wasm2js": "^1.1.1",
"preact": "^10.27.2",
"wasm-feature-detect": "^1.8.0"
"preact": "^10.27.2"
},
"devDependencies": {
"cssnano": "^7.1.1",
@@ -505,12 +503,6 @@
"node": ">=18"
}
},
"node_modules/@haribala/wasm2js": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@haribala/wasm2js/-/wasm2js-1.1.1.tgz",
"integrity": "sha512-PCZxbPNYJ3Nax1EPKb6oz2TSraSbhQyEXL2wZDlardsE7IwP6HHHVJVvDYWDz5p5HcASAvWRi74utGIepagTeA==",
"license": "Apache-2.0"
},
"node_modules/@smithy/is-array-buffer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
@@ -2693,12 +2685,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/wasm-feature-detect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@@ -8,20 +8,13 @@
"test:integration": "npm run assets && go test -v ./internal/test",
"test:integration:podman": "npm run assets && go test -v ./internal/test --playwright-runner=podman",
"test:integration:docker": "npm run assets && go test -v ./internal/test --playwright-runner=docker",
"generate": "go generate ./...",
"assets:js": "./web/build.sh",
"assets:css": "./xess/build.sh",
"assets:wasm": "bash ./scripts/build_wasm.sh",
"assets": "npm run generate && npm run assets:wasm && npm run assets:js && npm run assets:css",
"assets": "go generate ./... && ./web/build.sh && ./xess/build.sh",
"build": "npm run assets && go build -o ./var/anubis ./cmd/anubis",
"dev": "npm run assets && go run ./cmd/anubis --use-remote-address --target http://localhost:3000",
"container": "npm run assets && go run ./cmd/containerbuild",
"package": "yeet",
"lint": "make lint"
},
"imports": {
"lib/*": "./web/lib/*"
},
"author": "",
"license": "ISC",
"devDependencies": {
@@ -36,8 +29,6 @@
},
"dependencies": {
"@aws-crypto/sha256-js": "^5.2.0",
"@haribala/wasm2js": "^1.1.1",
"preact": "^10.27.2",
"wasm-feature-detect": "^1.8.0"
"preact": "^10.27.2"
}
}

View File

@@ -1,4 +0,0 @@
[toolchain]
channel = "stable"
components = ["rustfmt", "cargo", "clippy", "rust-analyzer", "rustfmt"]
targets = ["wasm32-unknown-unknown"]

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
mkdir -p ./web/static/wasm/{simd128,baseline}
cargo clean
# With simd128
RUSTFLAGS='-C target-feature=+simd128' cargo build --release --target wasm32-unknown-unknown
cp -vf ./target/wasm32-unknown-unknown/release/*.wasm ./web/static/wasm/simd128
cargo clean
# Without simd128
cargo build --release --target wasm32-unknown-unknown
cp -vf ./target/wasm32-unknown-unknown/release/*.wasm ./web/static/wasm/baseline
for file in ./web/static/wasm/baseline/*.wasm; do
echo $file
rm -f ${file%.*}.wasmjs
wasm2js $file -all -O4 --strip-debug --rse --rereloop --optimize-for-js --flatten --dce --dfo --fpcast-emu --denan --dealign --remove-imports --remove-unused-names --remove-unused-brs --reorder-functions --reorder-locals --strip-target-features --untee --vacuum -s 4 -ffm -lmu -tnh -iit -n -o ${file%.*}.mjs
sed -i '1s$.*$const anubis_update_nonce = (_ignored) => { };$' ${file%.*}.mjs
done

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Script to verify that the 'bots' field in data/botPolicies.yaml
has the same semantic contents as data/meta/default-config.yaml.
CW: generated by AI
"""
import yaml
import sys
import os
import subprocess
import difflib
def load_yaml(file_path):
"""Load YAML file and return the data."""
try:
with open(file_path, 'r') as f:
return yaml.safe_load(f)
except Exception as e:
print(f"Error loading {file_path}: {e}")
sys.exit(1)
def normalize_yaml(data):
"""Normalize YAML data by removing comments and standardizing structure."""
# For lists, just return as is, since YAML comments are stripped by safe_load
return data
def get_repo_root():
"""Get the root directory of the git repository."""
try:
result = subprocess.run(['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError:
print("Error: Not in a git repository")
sys.exit(1)
def main():
# Get the git repository root
repo_root = get_repo_root()
# Paths relative to the repo root
bot_policies_path = os.path.join(repo_root, 'data', 'botPolicies.yaml')
default_config_path = os.path.join(repo_root, 'data', 'meta', 'default-config.yaml')
# Load the files
bot_policies = load_yaml(bot_policies_path)
default_config = load_yaml(default_config_path)
# Extract the 'bots' field from botPolicies.yaml
if 'bots' not in bot_policies:
print("Error: 'bots' field not found in botPolicies.yaml")
sys.exit(1)
bots_field = bot_policies['bots']
# The default-config.yaml is a list directly
default_bots = default_config
# Normalize both
normalized_bots = normalize_yaml(bots_field)
normalized_default = normalize_yaml(default_bots)
# Compare
if normalized_bots == normalized_default:
print("SUCCESS: The 'bots' field in botPolicies.yaml matches the contents of default-config.yaml")
sys.exit(0)
else:
print("FAILURE: The 'bots' field in botPolicies.yaml does not match the contents of default-config.yaml")
print("\nDiff:")
bots_yaml = yaml.dump(normalized_bots, default_flow_style=False)
default_yaml = yaml.dump(normalized_default, default_flow_style=False)
diff = difflib.unified_diff(
bots_yaml.splitlines(keepends=True),
default_yaml.splitlines(keepends=True),
fromfile='bots field in botPolicies.yaml',
tofile='default-config.yaml'
)
print(''.join(diff))
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
python3 -c 'import yaml'
python3 ./compare_bots.py

View File

@@ -76,7 +76,6 @@ require (
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.4.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect

View File

@@ -211,8 +211,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=

5
test/ssh-ci/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
ARG ALPINE_VERSION=3.22
FROM alpine:${ALPINE_VERSION}
RUN apk add -U go nodejs git build-base git npm bash zstd brotli gzip
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"

View File

@@ -0,0 +1,26 @@
variable "ALPINE_VERSION" { default = "3.22" }
group "default" {
targets = [
"ci-runner",
]
}
target "ci-runner" {
args = {
ALPINE_VERSION = "3.22"
}
context = "."
dockerfile = "./Dockerfile"
platforms = [
"linux/amd64",
"linux/arm64",
"linux/arm/v7",
"linux/ppc64le",
"linux/riscv64",
]
pull = true
tags = [
"ghcr.io/techarohq/anubis/ci-runner:latest"
]
}

View File

@@ -5,4 +5,4 @@ set -x
npm ci
npm run build
SKIP_INTEGRATION=1 go test -v ./...
SKIP_INTEGRATION=1 go test ./...

View File

@@ -14,7 +14,7 @@ Hosts["ppc64le"]="ci@ppc64le.techaro.lol" # GOARCH=ppc64le GOOS=linux
Hosts["aarch64-4k"]="rocky@192.168.2.52" # GOARCH=arm64 GOOS=linux 4k page size
Hosts["aarch64-16k"]="ci@192.168.2.28" # GOARCH=arm64 GOOS=linux 16k page size
CIRunnerImage="ghcr.io/techarohq/ci-images/ssh-ci:latest"
CIRunnerImage="ghcr.io/techarohq/anubis/ci-runner:latest"
RunID=${GITHUB_RUN_ID:-$(uuidgen)}
RunFolder="anubis/runs/${RunID}"
Target="${Hosts["$1"]}"

View File

@@ -1,7 +0,0 @@
[package]
name = "anubis"
version = "0.1.0"
edition = "2024"
[dependencies]
wee_alloc = "0.4"

View File

@@ -1,72 +0,0 @@
use std::sync::{LazyLock, Mutex};
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[cfg(target_arch = "wasm32")]
mod hostimport {
use crate::{DATA_BUFFER, DATA_LENGTH};
#[link(wasm_import_module = "anubis")]
unsafe extern "C" {
/// The runtime expects this function to be defined. It is called whenever the Anubis check
/// worker processes about 1024 hashes. This can be a no-op if you want.
fn anubis_update_nonce(nonce: u32);
}
/// Safe wrapper to `anubis_update_nonce`.
pub fn update_nonce(nonce: u32) {
unsafe {
anubis_update_nonce(nonce);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn data_ptr() -> *const u8 {
let challenge = &DATA_BUFFER;
challenge.as_ptr()
}
#[unsafe(no_mangle)]
pub extern "C" fn set_data_length(len: u32) {
let mut data_length = DATA_LENGTH.lock().unwrap();
*data_length = len as usize;
}
}
#[cfg(not(target_arch = "wasm32"))]
mod hostimport {
use crate::{DATA_BUFFER, DATA_LENGTH};
pub fn update_nonce(_nonce: u32) {
// This is intentionally blank
}
pub fn data_ptr() -> *const u8 {
let challenge = &DATA_BUFFER;
challenge.as_ptr()
}
pub fn set_data_length(len: u32) {
let mut data_length = DATA_LENGTH.lock().unwrap();
*data_length = len as usize;
}
}
/// The data buffer is a bit weird in that it doesn't have an explicit length as it can
/// and will change depending on the challenge input that was sent by the server.
/// However, it can only fit 4096 bytes of data (one amd64 machine page). This is
/// slightly overkill for the purposes of an Anubis check, but it's fine to assume
/// that the browser can afford this much ram usage.
///
/// Callers should fetch the base data pointer, write up to 4096 bytes, and then
/// `set_data_length` the number of bytes they have written
///
/// This is also functionally a write-only buffer, so it doesn't really matter that
/// the length of this buffer isn't exposed.
pub static DATA_BUFFER: LazyLock<[u8; 4096]> = LazyLock::new(|| [0; 4096]);
pub static DATA_LENGTH: LazyLock<Mutex<usize>> = LazyLock::new(|| Mutex::new(0));
pub use hostimport::{data_ptr, set_data_length, update_nonce};

View File

@@ -1,22 +0,0 @@
[package]
name = "hashx"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
equix = "0.3"
hashx = "0.4"
anubis = { path = "../../anubis" }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
uninlined_format_args = "allow"
missing_panics_doc = "allow"
missing_errors_doc = "allow"
cognitive_complexity = "allow"

View File

@@ -1,156 +0,0 @@
use anubis::{DATA_BUFFER, DATA_LENGTH, update_nonce};
use hashx::HashX;
use std::boxed::Box;
use std::sync::{LazyLock, Mutex};
/// SHA-256 hashes are 32 bytes (256 bits). These are stored in static buffers due to the
/// fact that you cannot easily pass data from host space to WebAssembly space.
pub static RESULT_HASH: LazyLock<Box<Mutex<[u8; 32]>>> =
LazyLock::new(|| Box::new(Mutex::new([0; 32])));
pub static VERIFICATION_HASH: LazyLock<Box<Mutex<[u8; 32]>>> =
LazyLock::new(|| Box::new(Mutex::new([0; 32])));
/// Core validation function. Compare each bit in the hash by progressively masking bits until
/// some are found to not be matching.
///
/// There are probably more clever ways to do this, likely involving lookup tables or something
/// really fun like that. However in my testing this lets us get up to 200 kilohashes per second
/// on my Ryzen 7950x3D, up from about 50 kilohashes per second in JavaScript.
fn validate(hash: &[u8], difficulty: u32) -> bool {
let mut remaining = difficulty;
for &byte in hash {
// If we're out of bits to check, exit. This is all good.
if remaining == 0 {
break;
}
// If there are more than 8 bits remaining, the entire byte should be a
// zero. This fast-path compares the byte to 0 and if it matches, subtract
// 8 bits.
if remaining >= 8 {
if byte != 0 {
return false;
}
remaining -= 8;
} else {
// Otherwise mask off individual bits and check against them.
let mask = 0xFF << (8 - remaining);
if (byte & mask) != 0 {
return false;
}
remaining = 0;
}
}
true
}
fn anubis_work_inner(
difficulty: u32,
initial_nonce: u32,
iterand: u32,
) -> Result<u32, hashx::Error> {
let mut nonce: u32 = initial_nonce;
let data = &DATA_BUFFER;
let data_len = *DATA_LENGTH.lock().unwrap();
let data_slice = &data[..data_len];
let mut i = 0;
let h = HashX::new(data_slice)?;
loop {
i += 1;
let hash = h.hash_to_bytes(nonce as u64);
if validate(&hash, difficulty) {
// If the challenge worked, copy the bytes into `RESULT_HASH` so the runtime
// can pick it up.
let mut challenge = RESULT_HASH.lock().unwrap();
challenge.copy_from_slice(&hash);
return Ok(nonce);
}
nonce = nonce.wrapping_add(iterand);
if i == 1024 {
update_nonce(nonce);
i = 0;
}
}
}
/// This function is the main entrypoint for the Anubis proof of work implementation.
///
/// This expects `DATA_BUFFER` to be prepopulated with the challenge value as "raw bytes".
/// The definition of what goes in the data buffer is an exercise for the implementor, but
/// for SHA-256 we store the hash as "raw bytes". The data buffer is intentionally oversized
/// so that the challenge value can be expanded in the future.
///
/// `difficulty` is the number of leading bits that must match `0` in order for the
/// challenge to be successfully passed. This will be validated by the server.
///
/// `initial_nonce` is the initial value of the nonce (number used once). This nonce will be
/// appended to the challenge value in order to find a hash matching the specified
/// difficulty.
///
/// `iterand` (noun form of iterate) is the amount that the nonce should be increased by
/// every iteration of the proof of work loop. This will vary by how many threads are
/// running the proof-of-work check, and also functions as a thread ID. This prevents
/// wasting CPU time retrying a hash+nonce pair that likely won't work.
#[unsafe(no_mangle)]
pub extern "C" fn anubis_work(difficulty: u32, initial_nonce: u32, iterand: u32) -> u32 {
anubis_work_inner(difficulty, initial_nonce, iterand).unwrap()
}
/// This function is called by the server in order to validate a proof-of-work challenge.
/// This expects `DATA_BUFFER` to be set to the challenge value and `VERIFICATION_HASH` to
/// be set to the "raw bytes" of the SHA-256 hash that the client calculated.
///
/// If everything is good, it returns true. Otherwise, it returns false.
///
/// XXX(Xe): this could probably return an error code for what step fails, but this is fine
/// for now.
#[unsafe(no_mangle)]
pub extern "C" fn anubis_validate(nonce: u32, difficulty: u32) -> bool {
let data = &DATA_BUFFER;
let data_len = *DATA_LENGTH.lock().unwrap();
let data_slice = &data[..data_len];
let h: HashX = HashX::new(data_slice).unwrap();
let computed = h.hash_to_bytes(nonce as u64);
let valid = validate(&computed, difficulty);
if !valid {
return false;
}
let verification = VERIFICATION_HASH.lock().unwrap();
computed == *verification
}
// These functions exist to give pointers and lengths to the runtime around the Anubis
// checks, this allows JavaScript and Go to safely manipulate the memory layout that Rust
// has statically allocated at compile time without having to assume how the Rust compiler
// is going to lay it out.
#[unsafe(no_mangle)]
pub extern "C" fn result_hash_ptr() -> *const u8 {
let challenge = RESULT_HASH.lock().unwrap();
challenge.as_ptr()
}
#[unsafe(no_mangle)]
pub extern "C" fn result_hash_size() -> usize {
RESULT_HASH.lock().unwrap().len()
}
#[unsafe(no_mangle)]
pub extern "C" fn verification_hash_ptr() -> *const u8 {
let verification = VERIFICATION_HASH.lock().unwrap();
verification.as_ptr()
}
#[unsafe(no_mangle)]
pub extern "C" fn verification_hash_size() -> usize {
VERIFICATION_HASH.lock().unwrap().len()
}

View File

@@ -1,21 +0,0 @@
[package]
name = "sha256"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
sha2 = "0.11.0-pre.5"
anubis = { path = "../../anubis" }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
uninlined_format_args = "allow"
missing_panics_doc = "allow"
missing_errors_doc = "allow"
cognitive_complexity = "allow"

View File

@@ -1 +0,0 @@
<script src="run.js" type="module"></script>

View File

@@ -1,105 +0,0 @@
// Load and instantiate the .wasm file
const response = await fetch("sha256.wasm");
const importObject = {
anubis: {
anubis_update_nonce: (nonce) => {
console.log(`Received nonce update: ${nonce}`);
// Your logic here
}
}
};
const module = await WebAssembly.compileStreaming(response);
const instance = await WebAssembly.instantiate(module, importObject);
// Get exports
const {
anubis_work,
anubis_validate,
data_ptr,
result_hash_ptr,
result_hash_size,
verification_hash_ptr,
verification_hash_size,
set_data_length,
memory
} = instance.exports;
console.log(instance.exports);
function uint8ArrayToHex(arr) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
function hexToUint8Array(hexString) {
// Remove whitespace and optional '0x' prefix
hexString = hexString.replace(/\s+/g, '').replace(/^0x/, '');
// Check for valid length
if (hexString.length % 2 !== 0) {
throw new Error('Invalid hex string length');
}
// Check for valid characters
if (!/^[0-9a-fA-F]+$/.test(hexString)) {
throw new Error('Invalid hex characters');
}
// Convert to Uint8Array
const byteArray = new Uint8Array(hexString.length / 2);
for (let i = 0; i < byteArray.length; i++) {
const byteValue = parseInt(hexString.substr(i * 2, 2), 16);
byteArray[i] = byteValue;
}
return byteArray;
}
// Write data to buffer
function writeToBuffer(data) {
if (data.length > 1024) throw new Error("Data exceeds buffer size");
// Get pointer and create view
const offset = data_ptr();
const buffer = new Uint8Array(memory.buffer, offset, data.length);
// Copy data
buffer.set(data);
// Set data length
set_data_length(data.length);
}
function readFromChallenge() {
const offset = result_hash_ptr();
const buffer = new Uint8Array(memory.buffer, offset, result_hash_size());
return buffer;
}
// Example usage:
const data = hexToUint8Array("98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4");
writeToBuffer(data);
// Call work function
const t0 = Date.now();
const nonce = anubis_work(16, 0, 1);
const t1 = Date.now();
console.log(`Done! Took ${t1 - t0}ms, ${nonce} iterations`);
const challengeBuffer = readFromChallenge();
{
const buffer = new Uint8Array(memory.buffer, verification_hash_ptr(), verification_hash_size());
buffer.set(challengeBuffer);
}
// Validate
const isValid = anubis_validate(nonce, 10) === 1;
console.log(isValid);
console.log(uint8ArrayToHex(readFromChallenge()));

View File

@@ -1,166 +0,0 @@
use anubis::{DATA_BUFFER, DATA_LENGTH, update_nonce};
use sha2::{Digest, Sha256};
use std::boxed::Box;
use std::sync::{LazyLock, Mutex};
/// SHA-256 hashes are 32 bytes (256 bits). These are stored in static buffers due to the
/// fact that you cannot easily pass data from host space to WebAssembly space.
pub static RESULT_HASH: LazyLock<Box<Mutex<[u8; 32]>>> =
LazyLock::new(|| Box::new(Mutex::new([0; 32])));
pub static VERIFICATION_HASH: LazyLock<Box<Mutex<[u8; 32]>>> =
LazyLock::new(|| Box::new(Mutex::new([0; 32])));
/// Core validation function. Compare each bit in the hash by progressively masking bits until
/// some are found to not be matching.
///
/// There are probably more clever ways to do this, likely involving lookup tables or something
/// really fun like that. However in my testing this lets us get up to 200 kilohashes per second
/// on my Ryzen 7950x3D, up from about 50 kilohashes per second in JavaScript.
fn validate(hash: &[u8], difficulty: u32) -> bool {
let mut remaining = difficulty;
for &byte in hash {
// If we're out of bits to check, exit. This is all good.
if remaining == 0 {
break;
}
// If there are more than 8 bits remaining, the entire byte should be a
// zero. This fast-path compares the byte to 0 and if it matches, subtract
// 8 bits.
if remaining >= 8 {
if byte != 0 {
return false;
}
remaining -= 8;
} else {
// Otherwise mask off individual bits and check against them.
let mask = 0xFF << (8 - remaining);
if (byte & mask) != 0 {
return false;
}
remaining = 0;
}
}
true
}
/// Computes hash for given nonce.
///
/// This differs from the JavaScript implementations by constructing the hash differently. In
/// JavaScript implementations, the SHA-256 input is the result of appending the nonce as an
/// integer to the hex-formatted challenge, eg:
///
/// sha256(`${challenge}${nonce}`);
///
/// This **does work**, however I think that this can be done a bit better by operating on the
/// challenge bytes _directly_ and treating the nonce as a salt.
///
/// The nonce is also randomly encoded in either big or little endian depending on the last
/// byte of the data buffer in an effort to make it more annoying to automate with GPUs.
fn compute_hash(nonce: u32) -> [u8; 32] {
let data = &DATA_BUFFER;
let data_len = *DATA_LENGTH.lock().unwrap();
let use_le = data[data_len - 1] >= 128;
let data_slice = &data[..data_len];
let mut hasher = Sha256::new();
hasher.update(data_slice);
hasher.update(if use_le {
nonce.to_le_bytes()
} else {
nonce.to_be_bytes()
});
hasher.finalize().into()
}
/// This function is the main entrypoint for the Anubis proof of work implementation.
///
/// This expects `DATA_BUFFER` to be prepopulated with the challenge value as "raw bytes".
/// The definition of what goes in the data buffer is an exercise for the implementor, but
/// for SHA-256 we store the hash as "raw bytes". The data buffer is intentionally oversized
/// so that the challenge value can be expanded in the future.
///
/// `difficulty` is the number of leading bits that must match `0` in order for the
/// challenge to be successfully passed. This will be validated by the server.
///
/// `initial_nonce` is the initial value of the nonce (number used once). This nonce will be
/// appended to the challenge value in order to find a hash matching the specified
/// difficulty.
///
/// `iterand` (noun form of iterate) is the amount that the nonce should be increased by
/// every iteration of the proof of work loop. This will vary by how many threads are
/// running the proof-of-work check, and also functions as a thread ID. This prevents
/// wasting CPU time retrying a hash+nonce pair that likely won't work.
#[unsafe(no_mangle)]
pub extern "C" fn anubis_work(difficulty: u32, initial_nonce: u32, iterand: u32) -> u32 {
let mut nonce = initial_nonce;
let mut i = 0;
loop {
i += 1;
let hash = compute_hash(nonce);
if validate(&hash, difficulty) {
// If the challenge worked, copy the bytes into `RESULT_HASH` so the runtime
// can pick it up.
let mut challenge = RESULT_HASH.lock().unwrap();
challenge.copy_from_slice(&hash);
return nonce;
}
nonce = nonce.wrapping_add(iterand);
if i == 1024 {
update_nonce(nonce);
i = 0;
}
}
}
/// This function is called by the server in order to validate a proof-of-work challenge.
/// This expects `DATA_BUFFER` to be set to the challenge value and `VERIFICATION_HASH` to
/// be set to the "raw bytes" of the SHA-256 hash that the client calculated.
///
/// If everything is good, it returns true. Otherwise, it returns false.
///
/// XXX(Xe): this could probably return an error code for what step fails, but this is fine
/// for now.
#[unsafe(no_mangle)]
pub extern "C" fn anubis_validate(nonce: u32, difficulty: u32) -> bool {
let computed = compute_hash(nonce);
let valid = validate(&computed, difficulty);
if !valid {
return false;
}
let verification = VERIFICATION_HASH.lock().unwrap();
computed == *verification
}
// These functions exist to give pointers and lengths to the runtime around the Anubis
// checks, this allows JavaScript and Go to safely manipulate the memory layout that Rust
// has statically allocated at compile time without having to assume how the Rust compiler
// is going to lay it out.
#[unsafe(no_mangle)]
pub extern "C" fn result_hash_ptr() -> *const u8 {
let challenge = RESULT_HASH.lock().unwrap();
challenge.as_ptr()
}
#[unsafe(no_mangle)]
pub extern "C" fn result_hash_size() -> usize {
RESULT_HASH.lock().unwrap().len()
}
#[unsafe(no_mangle)]
pub extern "C" fn verification_hash_ptr() -> *const u8 {
let verification = VERIFICATION_HASH.lock().unwrap();
verification.as_ptr()
}
#[unsafe(no_mangle)]
pub extern "C" fn verification_hash_size() -> usize {
VERIFICATION_HASH.lock().unwrap().len()
}

View File

@@ -1,316 +0,0 @@
package wasm
import (
"context"
"errors"
"fmt"
"io"
"math"
"os"
"runtime"
"strconv"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
func UpdateNonce(uint32) {}
var (
validationTime = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "anubis_wasm_validation_time",
Help: "The time taken for the validation function to run per checker (nanoseconds)",
Buckets: prometheus.ExponentialBucketsRange(1, math.Pow(2, 31), 32),
}, []string{"fname"})
validationCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_wasm_validation",
Help: "The number of times the validation logic has been run and its success rate",
}, []string{"fname", "success"})
)
type Runner struct {
r wazero.Runtime
code wazero.CompiledModule
fname string
lock sync.Mutex
}
func NewRunner(ctx context.Context, fname string, fin io.ReadCloser) (*Runner, error) {
data, err := io.ReadAll(fin)
if err != nil {
return nil, fmt.Errorf("wasm: can't read from fin: %w", err)
}
var cfg wazero.RuntimeConfig
switch runtime.GOARCH {
case "amd64":
cfg = wazero.NewRuntimeConfigCompiler()
default:
cfg = wazero.NewRuntimeConfigInterpreter()
}
cfg = cfg.WithMemoryLimitPages(512)
r := wazero.NewRuntimeWithConfig(ctx, cfg)
_, err = r.NewHostModuleBuilder("anubis").
NewFunctionBuilder().
WithFunc(func(context.Context, uint32) {}).
Export("anubis_update_nonce").
Instantiate(ctx)
if err != nil {
return nil, fmt.Errorf("wasm: can't export anubis_update_nonce: %w", err)
}
code, err := r.CompileModule(ctx, data)
if err != nil {
return nil, fmt.Errorf("wasm: can't compile module: %w", err)
}
result := &Runner{
r: r,
code: code,
fname: fname,
}
return result, nil
}
func (r *Runner) checkExports(module api.Module) error {
funcs := []string{
"anubis_work",
"anubis_validate",
"data_ptr",
"set_data_length",
"result_hash_ptr",
"result_hash_size",
"verification_hash_ptr",
"verification_hash_size",
}
var errs []error
for _, fun := range funcs {
if module.ExportedFunction(fun) == nil {
errs = append(errs, fmt.Errorf("function %s is not defined", fun))
}
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func (r *Runner) anubisWork(ctx context.Context, module api.Module, difficulty, initialNonce, iterand uint32) (uint32, error) {
results, err := module.ExportedFunction("anubis_work").Call(ctx, uint64(difficulty), uint64(initialNonce), uint64(iterand))
if err != nil {
return 0, err
}
return uint32(results[0]), nil
}
func (r *Runner) anubisValidate(ctx context.Context, module api.Module, nonce, difficulty uint32) (bool, error) {
results, err := module.ExportedFunction("anubis_validate").Call(ctx, uint64(nonce), uint64(difficulty))
if err != nil {
return false, err
}
// Rust booleans are 1 if true
return results[0] == 1, nil
}
func (r *Runner) dataPtr(ctx context.Context, module api.Module) (uint32, error) {
results, err := module.ExportedFunction("data_ptr").Call(ctx)
if err != nil {
return 0, err
}
return uint32(results[0]), nil
}
func (r *Runner) setDataLength(ctx context.Context, module api.Module, length uint32) error {
_, err := module.ExportedFunction("set_data_length").Call(ctx, uint64(length))
return err
}
func (r *Runner) resultHashPtr(ctx context.Context, module api.Module) (uint32, error) {
results, err := module.ExportedFunction("result_hash_ptr").Call(ctx)
if err != nil {
return 0, err
}
return uint32(results[0]), nil
}
func (r *Runner) resultHashSize(ctx context.Context, module api.Module) (uint32, error) {
results, err := module.ExportedFunction("result_hash_size").Call(ctx)
if err != nil {
return 0, err
}
return uint32(results[0]), nil
}
func (r *Runner) verificationHashPtr(ctx context.Context, module api.Module) (uint32, error) {
results, err := module.ExportedFunction("verification_hash_ptr").Call(ctx)
if err != nil {
return 0, err
}
return uint32(results[0]), nil
}
func (r *Runner) verificationHashSize(ctx context.Context, module api.Module) (uint32, error) {
results, err := module.ExportedFunction("verification_hash_size").Call(ctx)
if err != nil {
return 0, err
}
return uint32(results[0]), nil
}
func (r *Runner) writeData(ctx context.Context, module api.Module, data []byte) error {
if len(data) > 4096 {
return os.ErrInvalid
}
length := uint32(len(data))
dataPtr, err := r.dataPtr(ctx, module)
if err != nil {
return fmt.Errorf("can't read data pointer: %w", err)
}
if !module.Memory().Write(dataPtr, data) {
return fmt.Errorf("[unexpected] can't write memory, is data out of range??")
}
if err := r.setDataLength(ctx, module, length); err != nil {
return fmt.Errorf("can't set data length: %w", err)
}
return nil
}
func (r *Runner) readResult(ctx context.Context, module api.Module) ([]byte, error) {
length, err := r.resultHashSize(ctx, module)
if err != nil {
return nil, fmt.Errorf("can't get result hash size: %w", err)
}
ptr, err := r.resultHashPtr(ctx, module)
if err != nil {
return nil, fmt.Errorf("can't get result hash pointer: %w", err)
}
buf, ok := module.Memory().Read(ptr, length)
if !ok {
return nil, fmt.Errorf("[unexpected] can't read from memory, is something out of range??")
}
return buf, nil
}
func (r *Runner) run(ctx context.Context, data []byte, difficulty, initialNonce, iterand uint32) (uint32, []byte, api.Module, error) {
mod, err := r.r.InstantiateModule(ctx, r.code, wazero.NewModuleConfig().WithName(r.fname))
if err != nil {
return 0, nil, nil, fmt.Errorf("can't instantiate module: %w", err)
}
if err := r.checkExports(mod); err != nil {
return 0, nil, nil, err
}
if err := r.writeData(ctx, mod, data); err != nil {
return 0, nil, nil, err
}
nonce, err := r.anubisWork(ctx, mod, difficulty, initialNonce, iterand)
if err != nil {
return 0, nil, nil, fmt.Errorf("can't run work function: %w", err)
}
hash, err := r.readResult(ctx, mod)
if err != nil {
return 0, nil, nil, fmt.Errorf("can't read result: %w", err)
}
return nonce, hash, mod, nil
}
func (r *Runner) Run(ctx context.Context, data []byte, difficulty, initialNonce, iterand uint32) (uint32, []byte, error) {
nonce, hash, _, err := r.run(ctx, data, difficulty, initialNonce, iterand)
if err != nil {
return 0, nil, fmt.Errorf("can't run %s: %w", r.fname, err)
}
return nonce, hash, nil
}
func (r *Runner) verify(ctx context.Context, data, verify []byte, nonce, difficulty uint32) (bool, api.Module, error) {
mod, err := r.r.InstantiateModule(ctx, r.code, wazero.NewModuleConfig().WithName(r.fname))
if err != nil {
return false, nil, fmt.Errorf("can't instantiate module: %w", err)
}
if err := r.checkExports(mod); err != nil {
return false, nil, err
}
if err := r.writeData(ctx, mod, data); err != nil {
return false, nil, err
}
if err := r.writeVerification(ctx, mod, verify); err != nil {
return false, nil, err
}
ok, err := r.anubisValidate(ctx, mod, nonce, difficulty)
if err != nil {
return false, nil, fmt.Errorf("can't validate hash %x from challenge %x, nonce %d and difficulty %d: %w", verify, data, nonce, difficulty, err)
}
return ok, mod, nil
}
func (r *Runner) Verify(ctx context.Context, data, verify []byte, nonce, difficulty uint32) (bool, error) {
r.lock.Lock()
defer r.lock.Unlock()
t0 := time.Now()
ok, _, err := r.verify(ctx, data, verify, nonce, difficulty)
validationTime.WithLabelValues(r.fname).Observe(float64(time.Since(t0)))
validationCount.WithLabelValues(r.fname, strconv.FormatBool(ok))
return ok, err
}
func (r *Runner) writeVerification(ctx context.Context, module api.Module, data []byte) error {
length, err := r.verificationHashSize(ctx, module)
if err != nil {
return fmt.Errorf("can't get verification hash size: %v", err)
}
if length != uint32(len(data)) {
return fmt.Errorf("data is too big, want %d bytes, got: %d", length, len(data))
}
ptr, err := r.verificationHashPtr(ctx, module)
if err != nil {
return fmt.Errorf("can't get verification hash pointer: %v", err)
}
if !module.Memory().Write(ptr, data) {
return fmt.Errorf("[unexpected] can't write memory, is data out of range??")
}
return nil
}

View File

@@ -1,181 +0,0 @@
package wasm
import (
"context"
"crypto/sha256"
"fmt"
"io/fs"
"path/filepath"
"testing"
"time"
"github.com/TecharoHQ/anubis/web"
)
func abiTest(t testing.TB, kind, fname string, difficulty uint32) {
fin, err := web.Static.Open("static/wasm/" + kind + "/" + fname)
if err != nil {
t.Fatal(err)
}
defer fin.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
t.Cleanup(cancel)
runner, err := NewRunner(ctx, fname, fin)
if err != nil {
t.Fatal(err)
}
h := sha256.New()
fmt.Fprint(h, t.Name())
data := h.Sum(nil)
nonce, hash, mod, err := runner.run(ctx, data, difficulty, 0, 1)
if err != nil {
t.Fatal(err)
}
if err := runner.writeVerification(ctx, mod, hash); err != nil {
t.Fatalf("can't write verification: %v", err)
}
ok, err := runner.anubisValidate(ctx, mod, nonce, difficulty)
if err != nil {
t.Fatalf("can't run validation: %v", err)
}
if !ok {
t.Error("validation failed")
}
t.Logf("used %d pages of wasm memory (%d bytes)", mod.Memory().Size()/63356, mod.Memory().Size())
}
func TestAlgos(t *testing.T) {
fnames, err := fs.ReadDir(web.Static, "static/wasm/baseline")
if err != nil {
t.Fatal(err)
}
for _, kind := range []string{"baseline", "simd128"} {
for _, fname := range fnames {
if filepath.Ext(fname.Name()) != ".wasm" {
continue
}
t.Run(kind+"/"+fname.Name(), func(t *testing.T) {
abiTest(t, kind, fname.Name(), 16)
})
}
}
}
func bench(b *testing.B, kind, fname string, difficulties []uint32) {
b.Helper()
fin, err := web.Static.Open("static/wasm/" + kind + "/" + fname)
if err != nil {
b.Fatal(err)
}
defer fin.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
b.Cleanup(cancel)
runner, err := NewRunner(ctx, fname, fin)
if err != nil {
b.Fatal(err)
}
h := sha256.New()
fmt.Fprint(h, "This is an example value that exists only to test the system.")
data := h.Sum(nil)
_, _, mod, err := runner.run(ctx, data, 0, 0, 1)
if err != nil {
b.Fatal(err)
}
for _, difficulty := range difficulties {
b.Run(fmt.Sprintf("difficulty/%d", difficulty), func(b *testing.B) {
for b.Loop() {
difficulty := difficulty
_, err := runner.anubisWork(ctx, mod, difficulty, 0, 1)
if err != nil {
b.Fatalf("can't do test work run: %v", err)
}
}
})
}
}
func BenchmarkSHA256(b *testing.B) {
for _, kind := range []string{"baseline", "simd128"} {
b.Run(kind, func(b *testing.B) {
bench(b, kind, "sha256.wasm", []uint32{4, 6, 8, 10, 12, 14, 16})
})
}
}
func BenchmarkHashX(b *testing.B) {
for _, kind := range []string{"baseline", "simd128"} {
b.Run(kind, func(b *testing.B) {
bench(b, kind, "hashx.wasm", []uint32{4, 6, 8, 10, 12, 14, 16})
})
}
}
func BenchmarkValidate(b *testing.B) {
fnames, err := fs.ReadDir(web.Static, "static/wasm/simd128")
if err != nil {
b.Fatal(err)
}
h := sha256.New()
fmt.Fprint(h, "This is an example value that exists only to test the system.")
data := h.Sum(nil)
for _, fname := range fnames {
fname := fname.Name()
difficulty := uint32(1)
switch fname {
case "sha256.wasm":
difficulty = 16
}
if filepath.Ext(fname) != ".wasm" {
continue
}
b.Run(fname, func(b *testing.B) {
fin, err := web.Static.Open("static/wasm/simd128/" + fname)
if err != nil {
b.Fatal(err)
}
defer fin.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
b.Cleanup(cancel)
runner, err := NewRunner(ctx, fname, fin)
if err != nil {
b.Fatal(err)
}
nonce, hash, mod, err := runner.run(ctx, data, difficulty, 0, 1)
if err != nil {
b.Fatal(err)
}
if err := runner.writeVerification(ctx, mod, hash); err != nil {
b.Fatalf("can't write verification: %v", err)
}
for b.Loop() {
_, err := runner.anubisValidate(ctx, mod, nonce, difficulty)
if err != nil {
b.Fatalf("can't run validation: %v", err)
}
}
})
}
}

View File

@@ -31,9 +31,6 @@ THE SOFTWARE.
Includes code from https://github.com/aws/aws-sdk-js-crypto-helpers which is
used under the terms of the Apache 2 license.
Includes code written in Rust and transpiled from WebAssembly to JavaScript
which is used under the terms of the licenses that comprise those crates.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/'
@@ -52,7 +49,7 @@ for file in js/**/*.ts js/**/*.mjs; do
mkdir -p "$(dirname "$out")"
esbuild "$file" --sourcemap --minify --bundle --outfile="$out" --banner:js="$LICENSE"
esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE"
gzip -f -k -n "$out"
zstd -f -k --ultra -22 "$out"
brotli -fZk "$out"

View File

@@ -18,7 +18,12 @@ export default function process(
): Promise<string> {
console.debug("fast algo");
let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs";
// Choose worker based on secure context.
// Use the WebCrypto worker if the page is a secure context; otherwise fall back to pureJS.
let workerMethod: "webcrypto" | "purejs" = "purejs";
if (window.isSecureContext) {
workerMethod = "webcrypto";
}
if (navigator.userAgent.includes("Firefox") || navigator.userAgent.includes("Goanna")) {
console.log("Firefox detected, using pure-JS fallback");
@@ -82,4 +87,4 @@ export default function process(
workers.push(worker);
}
});
}
}

View File

@@ -1,10 +1,6 @@
import fast from "./fast";
import wasm from "./wasm";
export default {
fast: fast,
slow: fast, // XXX(Xe): slow is deprecated, but keep this around in case anything goes bad
hashx: wasm,
sha256: wasm,
}

View File

@@ -1,23 +0,0 @@
import {
memory,
data_ptr,
set_data_length,
anubis_work,
anubis_validate,
result_hash_ptr,
result_hash_size,
verification_hash_ptr,
verification_hash_size,
} from "../../../static/wasm/baseline/hashx.mjs";
export default {
memory,
data_ptr,
set_data_length,
anubis_work,
anubis_validate,
result_hash_ptr,
result_hash_size,
verification_hash_ptr,
verification_hash_size
};

View File

@@ -1,118 +0,0 @@
import { u } from "../../../lib/xeact";
import { simd } from "wasm-feature-detect";
// import { compile } from '@haribala/wasm2js';
type ProgressCallback = (nonce: number | string) => void;
interface ProcessOptions {
basePrefix: string;
version: string;
algorithm: string;
}
const getHardwareConcurrency = () =>
navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1;
// https://stackoverflow.com/questions/47879864/how-can-i-check-if-a-browser-supports-webassembly
const isWASMSupported = (() => {
try {
if (typeof WebAssembly === "object"
&& typeof WebAssembly.instantiate === "function") {
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
if (module instanceof WebAssembly.Module)
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
} catch (e) {
return false;
}
return false;
})();
export default function process(
options: ProcessOptions,
data: string,
difficulty: number = 5,
signal: AbortSignal | null = null,
progressCallback?: ProgressCallback,
threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)),
): Promise<string> {
const { basePrefix, version, algorithm } = options;
let worker = "wasm";
if (!isWASMSupported) {
worker = "wasm2js";
}
return new Promise(async (resolve, reject) => {
let wasmFeatures = "baseline";
if (await simd()) {
wasmFeatures = "simd128";
}
let module = undefined;
if (isWASMSupported) {
module = await fetch(u(`${basePrefix}/.within.website/x/cmd/anubis/static/wasm/${wasmFeatures}/${algorithm}.wasm?cacheBuster=${version}`))
.then(x => WebAssembly.compileStreaming(x));
}
const webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/${worker}.mjs?cacheBuster=${version}`;
const workers: Worker[] = [];
let settled = false;
const onAbort = () => {
console.log("PoW aborted");
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
const cleanup = () => {
if (settled) {
return;
}
settled = true;
workers.forEach((w) => w.terminate());
if (signal != null) {
signal.removeEventListener("abort", onAbort);
}
};
if (signal != null) {
if (signal.aborted) {
return onAbort();
}
signal.addEventListener("abort", onAbort, { once: true });
}
for (let i = 0; i < threads; i++) {
let worker = new Worker(webWorkerURL);
worker.onmessage = (event) => {
if (typeof event.data === "number") {
progressCallback?.(event.data);
} else {
cleanup();
resolve(event.data);
}
}
worker.onerror = (event) => {
cleanup();
reject(event);
}
worker.postMessage({
data,
difficulty,
nonce: i,
threads,
algorithm,
module,
});
}
});
};

View File

@@ -1,23 +0,0 @@
import {
memory,
data_ptr,
set_data_length,
anubis_work,
anubis_validate,
result_hash_ptr,
result_hash_size,
verification_hash_ptr,
verification_hash_size,
} from "../../../static/wasm/baseline/sha256.mjs";
export default {
memory,
data_ptr,
set_data_length,
anubis_work,
anubis_validate,
result_hash_ptr,
result_hash_size,
verification_hash_ptr,
verification_hash_size
};

View File

@@ -171,7 +171,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
try {
const t0 = Date.now();
const { hash, nonce } = await process(
{ basePrefix, version: anubisVersion, algorithm: rules.algorithm },
{ basePrefix, version: anubisVersion },
challenge.randomData,
rules.difficulty,
null,

View File

@@ -1,105 +0,0 @@
export interface Args {
data: string;
difficulty: number;
nonce: number;
threads: number;
module: BufferSource;
}
interface AnubisExports {
anubis_work: (difficulty: number, initialNonce: number, threads: number) => number;
data_ptr: () => number;
result_hash_ptr: () => number;
result_hash_size: () => number;
set_data_length: (len: number) => void;
memory: WebAssembly.Memory;
}
addEventListener("message", async (event: MessageEvent<Args>) => {
const { data, difficulty, threads, module } = event.data;
let { nonce } = event.data;
const importObject = {
anubis: {
anubis_update_nonce: (nonce: number) => postMessage(nonce),
}
};
if (nonce !== 0) {
importObject.anubis.anubis_update_nonce = (_) => { };
}
const obj = await WebAssembly.instantiate(module, importObject);
const {
anubis_work,
data_ptr,
result_hash_ptr,
result_hash_size,
set_data_length,
memory
} = (obj as unknown as any).exports as unknown as AnubisExports;
function uint8ArrayToHex(arr: Uint8Array) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
function hexToUint8Array(hexString: string): Uint8Array {
// Remove whitespace and optional '0x' prefix
hexString = hexString.replace(/\s+/g, '').replace(/^0x/, '');
// Check for valid length
if (hexString.length % 2 !== 0) {
throw new Error('Invalid hex string length');
}
// Check for valid characters
if (!/^[0-9a-fA-F]+$/.test(hexString)) {
throw new Error('Invalid hex characters');
}
// Convert to Uint8Array
const byteArray = new Uint8Array(hexString.length / 2);
for (let i = 0; i < byteArray.length; i++) {
const byteValue = parseInt(hexString.substr(i * 2, 2), 16);
byteArray[i] = byteValue;
}
return byteArray;
}
// Write data to buffer
function writeToBuffer(data: Uint8Array) {
if (data.length > 1024) throw new Error("Data exceeds buffer size");
// Get pointer and create view
const offset = data_ptr();
const buffer = new Uint8Array(memory.buffer, offset, data.length);
// Copy data
buffer.set(data);
// Set data length
set_data_length(data.length);
}
function readFromChallenge() {
const offset = result_hash_ptr();
const buffer = new Uint8Array(memory.buffer, offset, result_hash_size());
return buffer;
}
writeToBuffer(hexToUint8Array(data));
nonce = anubis_work(difficulty, nonce, threads);
const challenge = readFromChallenge();
const result = uint8ArrayToHex(challenge);
postMessage({
hash: result,
difficulty,
nonce,
});
});

View File

@@ -1,106 +0,0 @@
import hashx from "../algorithms/wasm/hashx";
import sha256 from "../algorithms/wasm/sha256";
export interface Args {
data: string;
difficulty: number;
nonce: number;
threads: number;
algorithm: string;
}
interface AnubisExports {
anubis_work: (difficulty: number, initialNonce: number, threads: number) => number;
data_ptr: () => number;
result_hash_ptr: () => number;
result_hash_size: () => number;
set_data_length: (len: number) => void;
memory: WebAssembly.Memory;
}
const algorithms: Record<string, AnubisExports> = {
"hashx": hashx as AnubisExports,
"sha256": sha256 as AnubisExports,
};
addEventListener("message", async (event: MessageEvent<Args>) => {
const { data, difficulty, threads, algorithm } = event.data;
let { nonce } = event.data;
const obj = algorithms[algorithm];
if (obj == undefined) {
throw new Error(`unknown algorithm ${algorithm}, file a bug please`);
}
const {
anubis_work,
data_ptr,
result_hash_ptr,
result_hash_size,
set_data_length,
memory
} = obj;
function uint8ArrayToHex(arr: Uint8Array) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
function hexToUint8Array(hexString: string): Uint8Array {
// Remove whitespace and optional '0x' prefix
hexString = hexString.replace(/\s+/g, '').replace(/^0x/, '');
// Check for valid length
if (hexString.length % 2 !== 0) {
throw new Error('Invalid hex string length');
}
// Check for valid characters
if (!/^[0-9a-fA-F]+$/.test(hexString)) {
throw new Error('Invalid hex characters');
}
// Convert to Uint8Array
const byteArray = new Uint8Array(hexString.length / 2);
for (let i = 0; i < byteArray.length; i++) {
const byteValue = parseInt(hexString.substr(i * 2, 2), 16);
byteArray[i] = byteValue;
}
return byteArray;
}
// Write data to buffer
function writeToBuffer(data: Uint8Array) {
if (data.length > 1024) throw new Error("Data exceeds buffer size");
// Get pointer and create view
const offset = data_ptr();
const buffer = new Uint8Array(memory.buffer, offset, data.length);
// Copy data
buffer.set(data);
// Set data length
set_data_length(data.length);
}
function readFromChallenge() {
const offset = result_hash_ptr();
const buffer = new Uint8Array(memory.buffer, offset, result_hash_size());
return buffer;
}
writeToBuffer(hexToUint8Array(data));
nonce = anubis_work(difficulty, nonce, threads);
const challenge = readFromChallenge();
const result = uint8ArrayToHex(challenge);
postMessage({
hash: result,
difficulty,
nonce,
});
});

View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -16,7 +16,8 @@ var (
//go:embed *.css static
Static embed.FS
URL = "/.within.website/x/xess/xess.css"
BasePrefix = "/.within.website/x/xess/"
URL = "/.within.website/x/xess/xess.css"
)
func init() {