Compare commits

..

27 Commits

Author SHA1 Message Date
Xe Iaso
8cc2c4d07c ci: fix smoke tests
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 21:17:36 +00:00
Xe Iaso
158a3b8d77 docs: update CHANGELOG with binaryen dependency
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 21:04:43 +00:00
Xe Iaso
1530eab1a4 docs: update CHANGELOG
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 21:02:59 +00:00
Xe Iaso
accffa6d83 chore(wasm): spelling fixes
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 20:57:59 +00:00
Xe Iaso
c04bd486e1 fix(wasm): support loading webassembly again
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 20:57:49 +00:00
Xe Iaso
0699f331d2 docs: add wasm documentation
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 20:57:10 +00:00
Xe Iaso
7f1a7197f3 fix(wasm): use interpreter on aarch64 for now
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 14:39:56 +00:00
Xe Iaso
2ad7be2847 ci: use binaryen 108
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 14:33:02 +00:00
Xe Iaso
45c9cb6842 Update metadata
check-spelling run (pull_request) for Xe/wasm3

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>
2025-09-30 14:30:59 +00:00
Xe Iaso
a29025c382 ci: fix invocations of setup-binaryen
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 14:28:39 +00:00
Xe Iaso
bba000e87e chore: clean up places I forced things in testing
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 14:27:19 +00:00
Xe Iaso
643b4719d8 feat(wasm): support "pure JS" mode
Closes #1159

This uses the binaryen tool wasm2js to compile the Anubis WASM blobs
to JavaScript. This produces biblically large (520Ki) outputs when you
inline both hashx and sha256 solvers, but this is a tradeoff that I'm
willing to accept. The performance is good enough in my testing with
JIT enabled. I fear that this may end up being terrible with JIT
disabled. I have no idea if this will work on big endian or not.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-30 14:21:29 +00:00
Xe Iaso
705da2fa3c fix: disable broken wasm interpreter flow for now
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 15:01:42 +00:00
Xe Iaso
bd5613c699 feat(web/wasm): start work on wasm2js, found bugs in the code, stopping to go to bed
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 04:25:26 +00:00
Xe Iaso
a0df3d4428 chore: cleanups
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 04:03:39 +00:00
Xe Iaso
097c9e9586 feat(web/wasm): use simd128 if available
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 04:03:30 +00:00
Xe Iaso
cf931cc0a5 fix(lib): detect failures on challenge method initialization
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 04:02:49 +00:00
Xe Iaso
8b60b4309b chore(wasm): argon2id -> hashx
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 03:39:19 +00:00
Xe Iaso
41bfbf7900 test(ssh-ci): use TecharoHQ/ci-images SSH runner image
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 03:09:45 +00:00
Xe Iaso
1c5ce190b4 ci: test ssh ci for the wasm stack
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 02:50:43 +00:00
Xe Iaso
bed126e641 ci(packages): fix builds
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 02:48:00 +00:00
Xe Iaso
9959cb0d06 ci: fix rust wasm32 target
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 02:45:17 +00:00
Xe Iaso
f150e4b466 ci: fix rust dependencies
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 02:40:54 +00:00
Xe Iaso
8999303eef feat(lib/challenge/wasm): server side validation logic
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 02:35:42 +00:00
Xe Iaso
a63cbc7ced feat(web/js): add wasm client side runner
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-28 02:21:11 +00:00
Xe Iaso
03a6c07c73 chore: add rust-toolchain.toml
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-27 18:33:05 +00:00
Xe Iaso
908f85db91 feat: add wasm rigging
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-27 17:50:28 +00:00
80 changed files with 2929 additions and 785 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ name: Docker image builds (pull requests)
on: on:
pull_request: pull_request:
branches: [ "main" ] branches: ["main"]
env: env:
DOCKER_METADATA_SET_OUTPUT_ENV: "true" DOCKER_METADATA_SET_OUTPUT_ENV: "true"
@@ -45,6 +45,17 @@ jobs:
run: | run: |
brew bundle 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 - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0

View File

@@ -55,6 +55,17 @@ jobs:
run: | run: |
brew bundle 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 - name: Log into registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:

View File

@@ -2,9 +2,9 @@ name: Go
on: on:
push: push:
branches: [ "main" ] branches: ["main"]
pull_request: pull_request:
branches: [ "main" ] branches: ["main"]
permissions: permissions:
contents: read contents: read
@@ -15,77 +15,88 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: build essential - name: build essential
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- name: Set up Homebrew - name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@main uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache - name: Setup Homebrew cellar cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
/home/linuxbrew/.linuxbrew/Cellar /home/linuxbrew/.linuxbrew/Cellar
/home/linuxbrew/.linuxbrew/bin /home/linuxbrew/.linuxbrew/bin
/home/linuxbrew/.linuxbrew/etc /home/linuxbrew/.linuxbrew/etc
/home/linuxbrew/.linuxbrew/include /home/linuxbrew/.linuxbrew/include
/home/linuxbrew/.linuxbrew/lib /home/linuxbrew/.linuxbrew/lib
/home/linuxbrew/.linuxbrew/opt /home/linuxbrew/.linuxbrew/opt
/home/linuxbrew/.linuxbrew/sbin /home/linuxbrew/.linuxbrew/sbin
/home/linuxbrew/.linuxbrew/share /home/linuxbrew/.linuxbrew/share
/home/linuxbrew/.linuxbrew/var /home/linuxbrew/.linuxbrew/var
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }} key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go-homebrew-cellar- ${{ runner.os }}-go-homebrew-cellar-
- name: Install Brew dependencies - name: Install Brew dependencies
run: | run: |
brew bundle brew bundle
- name: Setup Golang caches - name: Setup Golang caches
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
~/.cache/go-build ~/.cache/go-build
~/go/pkg/mod ~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-golang- ${{ runner.os }}-golang-
- name: Cache playwright binaries - uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with:
id: playwright-cache cache: false
with: target: wasm32-unknown-unknown
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
- name: install node deps - name: Setup Binaryen
run: | uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
npm ci with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- name: install playwright browsers - name: Cache playwright binaries
run: | uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
npx --no-install playwright@1.52.0 install --with-deps id: playwright-cache
npx --no-install playwright@1.52.0 run-server --port 9001 & with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
- name: Build - name: install node deps
run: npm run build run: |
npm ci
- name: Test - name: install playwright browsers
run: npm run test run: |
npx --no-install playwright@1.52.0 install --with-deps
npx --no-install playwright@1.52.0 run-server --port 9001 &
- name: Lint with staticcheck - name: Build
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0 run: npm run build
with:
version: "latest"
- name: Govulncheck - name: Test
run: | run: npm run test
go tool govulncheck ./...
- name: Lint with staticcheck
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0
with:
version: "latest"
- name: Govulncheck
run: |
go tool govulncheck ./...

View File

@@ -59,6 +59,17 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-golang- ${{ 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 - name: install node deps
run: | run: |
npm ci npm ci

View File

@@ -2,9 +2,9 @@ name: Package builds (unstable)
on: on:
push: push:
branches: [ "main" ] branches: ["main"]
pull_request: pull_request:
branches: [ "main" ] branches: ["main"]
permissions: permissions:
contents: read contents: read
@@ -15,60 +15,71 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
- name: build essential - name: build essential
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- name: Set up Homebrew - name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@main uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache - name: Setup Homebrew cellar cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
/home/linuxbrew/.linuxbrew/Cellar /home/linuxbrew/.linuxbrew/Cellar
/home/linuxbrew/.linuxbrew/bin /home/linuxbrew/.linuxbrew/bin
/home/linuxbrew/.linuxbrew/etc /home/linuxbrew/.linuxbrew/etc
/home/linuxbrew/.linuxbrew/include /home/linuxbrew/.linuxbrew/include
/home/linuxbrew/.linuxbrew/lib /home/linuxbrew/.linuxbrew/lib
/home/linuxbrew/.linuxbrew/opt /home/linuxbrew/.linuxbrew/opt
/home/linuxbrew/.linuxbrew/sbin /home/linuxbrew/.linuxbrew/sbin
/home/linuxbrew/.linuxbrew/share /home/linuxbrew/.linuxbrew/share
/home/linuxbrew/.linuxbrew/var /home/linuxbrew/.linuxbrew/var
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }} key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go-homebrew-cellar- ${{ runner.os }}-go-homebrew-cellar-
- name: Install Brew dependencies - name: Install Brew dependencies
run: | run: |
brew bundle brew bundle
- name: Setup Golang caches - name: Setup Golang caches
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
~/.cache/go-build ~/.cache/go-build
~/go/pkg/mod ~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-golang- ${{ runner.os }}-golang-
- name: install node deps - uses: actions-rust-lang/setup-rust-toolchain@02be93da58aa71fb456aa9c43b301149248829d8 # v1.15.1
run: | with:
npm ci cache: false
target: wasm32-unknown-unknown
- name: Build Packages - name: Setup Binaryen
run: | uses: Aandreba/setup-binaryen@77f25f9d7d30f09667a2535888bf9516b31a4cd7 # v1.0.0
go tool yeet with:
token: ${{ secrets.GITHUB_TOKEN }}
version: 108
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - name: install node deps
with: run: |
name: packages npm ci
path: var/*
- name: Build Packages
run: |
go tool yeet
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: packages
path: var/*

35
.github/workflows/rust.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
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,7 +14,6 @@ jobs:
strategy: strategy:
matrix: matrix:
test: test:
- default-config-macro
- double_slash - double_slash
- forced-language - forced-language
- git-clone - git-clone
@@ -38,6 +37,11 @@ jobs:
with: with:
go-version: stable 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 - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- name: Install utils - name: Install utils

View File

@@ -1,37 +0,0 @@
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: on:
push: push:
branches: ["main"] branches: ["main"]
# pull_request: pull_request:
# branches: ["main"] branches: ["main"]
permissions: permissions:
contents: read contents: read
@@ -39,6 +39,17 @@ jobs:
with: with:
go-version: stable 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 - name: Run CI
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }} run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
env: env:

6
.gitignore vendored
View File

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

431
Cargo.lock generated Normal file
View File

@@ -0,0 +1,431 @@
# 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"

10
Cargo.toml Normal file
View File

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

View File

@@ -1 +1 @@
1.23.0-pre1 1.22.0

View File

@@ -11,9 +11,6 @@
## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from. ## /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
bots: bots:
# You can import the entire default config with this macro:
# - import: (data)/meta/default-config.yaml
# Pathological bots to deny # Pathological bots to deny
- # This correlates to data/bots/_deny-pathological.yaml in the source tree - # This correlates to data/bots/_deny-pathological.yaml in the source tree
# https://github.com/TecharoHQ/anubis/blob/main/data/bots/_deny-pathological.yaml # https://github.com/TecharoHQ/anubis/blob/main/data/bots/_deny-pathological.yaml
@@ -96,50 +93,6 @@ bots:
# weight: # weight:
# adjust: -10 # 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 # Generic catchall rule
- name: generic-browser - name: generic-browser
user_agent_regex: >- user_agent_regex: >-
@@ -259,10 +212,14 @@ thresholds:
- weight < 20 - weight < 20
action: CHALLENGE action: CHALLENGE
challenge: challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work # https://anubis.techaro.lol/docs/admin/configuration/challenges/preact
algorithm: fast #
difficulty: 2 # two leading zeros, very fast for most clients # This challenge proves the client can run a webapp written with Preact.
report_as: 2 # 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
- name: mild-proof-of-work - name: mild-proof-of-work
expression: expression:
all: all:
@@ -272,8 +229,8 @@ thresholds:
challenge: challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work # https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast algorithm: fast
difficulty: 4 difficulty: 2 # two leading zeros, very fast for most clients
report_as: 4 report_as: 2
# For clients that are browser like and have gained many points from custom rules # For clients that are browser like and have gained many points from custom rules
- name: extreme-suspicion - name: extreme-suspicion
expression: weight >= 30 expression: weight >= 30
@@ -281,5 +238,5 @@ thresholds:
challenge: challenge:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work # https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast algorithm: fast
difficulty: 6 difficulty: 4
report_as: 6 report_as: 4

View File

@@ -2,19 +2,13 @@
action: ALLOW action: ALLOW
expression: expression:
all: all:
- > - >
( (
userAgent.startsWith("git/") || userAgent.startsWith("git/") ||
userAgent.contains("libgit") || userAgent.contains("libgit") ||
userAgent.startsWith("go-git") || userAgent.startsWith("go-git") ||
userAgent.startsWith("JGit/") || userAgent.startsWith("JGit/") ||
userAgent.startsWith("JGit-") userAgent.startsWith("JGit-")
) )
- '"Accept" in headers' - '"Git-Protocol" in headers'
- headers["Accept"] == "*/*" - headers["Git-Protocol"] == "version=2"
- '"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

@@ -1,133 +0,0 @@
- # 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,19 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- This changes the project to: --> <!-- This changes the project to: -->
## v1.23.0: Lyse Hext
- 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 `-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 `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. - Add `COOKIE_SAME_SITE_MODE` to force anubis cookies SameSite value, and downgrade automatically from `None` to `Lax` if cookie is insecure.
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)). - Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
- Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)). - Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
- Remove bbolt actorify implementation due to causing production issues.
- 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)). - 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. - 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)). - 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. - 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`. - 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. - Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines.
@@ -34,15 +29,25 @@ 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)). - 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. - 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. - 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)). - 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)). - 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)).
- The Preact challenge has been removed from the default configuration. It will be deprecated in the future.
### Better error messages ### Major new features
In order to make it easier for legitimate clients to debug issues with their browser configuration and Anubis, Anubis will emit internal error detail in base 64 so that administrators can chase down issues. Future versions of this may also include a variant that encrypts the error detail messages. #### 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.
### Bug Fixes ### Bug Fixes

View File

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

View File

@@ -0,0 +1,66 @@
# 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 Validation
participant Evil Site participant Evil Site
Hacker->>+User: Click on example.org with this solution Hacker->>+User: Click on yoursite.com with this solution
User->>+Validation: Here's a solution, send me to evilsite.com User->>+Validation: Here's a solution, send me to evilsite.com
Validation->>+User: Here's a cookie, go to evilsite.com Validation->>+User: Here's a cookie, go to evilsite.com
User->>+Evil Site: GET evilsite.com User->>+Evil Site: GET evilsite.com
@@ -46,14 +46,11 @@ Redirect domain not allowed
## Configuring allowed redirect domains ## Configuring allowed redirect domains
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. 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.
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 :::note
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. These domains are _an exact string match_, they do not support wildcard matches.
::: :::
@@ -63,7 +60,7 @@ If you are hosting Anubis on a non-standard port (`https://example:com:8443`, `h
```shell ```shell
# anubis.env # anubis.env
REDIRECT_DOMAINS="example.org,secretplans.example.org,*.test.example.org" REDIRECT_DOMAINS="yoursite.com,secretplans.yoursite.com"
# ... # ...
``` ```
@@ -75,7 +72,7 @@ services:
anubis-nginx: anubis-nginx:
image: ghcr.io/techarohq/anubis:latest image: ghcr.io/techarohq/anubis:latest
environment: environment:
REDIRECT_DOMAINS: "example.org,secretplans.example.org,*.test.example.org" REDIRECT_DOMAINS: "yoursite.com,secretplans.yoursite.com"
# ... # ...
``` ```
@@ -89,7 +86,7 @@ Inside your Deployment, StatefulSet, or Pod:
image: ghcr.io/techarohq/anubis:latest image: ghcr.io/techarohq/anubis:latest
env: env:
- name: REDIRECT_DOMAINS - name: REDIRECT_DOMAINS
value: "example.org,secretplans.example.org,*.test.example.org" value: "yoursite.com,secretplans.yoursite.com"
# ... # ...
``` ```

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. | | `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. | | `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). | | `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). |
| `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. | | `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.<br/><br/>Note that if you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here. |
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. | | `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
| `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. | | `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. | | `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

@@ -0,0 +1,75 @@
# 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,6 +25,7 @@ require (
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
github.com/shirou/gopsutil/v4 v4.25.6 github.com/shirou/gopsutil/v4 v4.25.6
github.com/testcontainers/testcontainers-go v0.38.0 github.com/testcontainers/testcontainers-go v0.38.0
github.com/tetratelabs/wazero v1.9.0
go.etcd.io/bbolt v1.4.2 go.etcd.io/bbolt v1.4.2
golang.org/x/net v0.42.0 golang.org/x/net v0.42.0
golang.org/x/text v0.27.0 golang.org/x/text v0.27.0
@@ -87,7 +88,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/docker v28.3.3+incompatible // indirect github.com/docker/docker v28.3.2+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 // 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/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 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -395,6 +395,8 @@ 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/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 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= 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 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=

View File

@@ -37,6 +37,7 @@ import (
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh" _ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
_ "github.com/TecharoHQ/anubis/lib/challenge/preact" _ "github.com/TecharoHQ/anubis/lib/challenge/preact"
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork" _ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
_ "github.com/TecharoHQ/anubis/lib/challenge/wasm"
) )
var ( var (
@@ -164,7 +165,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
if err != nil { if err != nil {
lg.Error("check failed", "err", err) lg.Error("check failed", "err", err)
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy\"", localizer.T("internal_server_error")), makeCode(err)) s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy\"", localizer.T("internal_server_error")))
return return
} }
@@ -267,13 +268,13 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
lg.Info("explicit deny") lg.Info("explicit deny")
if rule == nil { if rule == nil {
lg.Error("rule is nil, cannot calculate checksum") lg.Error("rule is nil, cannot calculate checksum")
s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.RuleDeny\"", localizer.T("internal_server_error")), makeCode(ErrActualAnubisBug)) s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.RuleDeny\"", localizer.T("internal_server_error")))
return true return true
} }
hash := rule.Hash() hash := rule.Hash()
lg.Debug("rule hash", "hash", hash) lg.Debug("rule hash", "hash", hash)
s.respondWithStatus(w, r, fmt.Sprintf("%s %s", localizer.T("access_denied"), hash), "", s.policy.StatusCodes.Deny) s.respondWithStatus(w, r, fmt.Sprintf("%s %s", localizer.T("access_denied"), hash), s.policy.StatusCodes.Deny)
return true return true
case config.RuleChallenge: case config.RuleChallenge:
lg.Debug("challenge requested") lg.Debug("challenge requested")
@@ -284,7 +285,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
default: default:
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host}) s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
lg.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule) lg.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule)
s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.Rules\"", localizer.T("internal_server_error")), makeCode(ErrActualAnubisBug)) s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.Rules\"", localizer.T("internal_server_error")))
return true return true
} }
return false return false
@@ -311,7 +312,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
localizer.T("dronebl_entry"), localizer.T("dronebl_entry"),
resp.String(), resp.String(),
localizer.T("see_dronebl_lookup"), localizer.T("see_dronebl_lookup"),
ip), "", s.policy.StatusCodes.Deny) ip), s.policy.StatusCodes.Deny)
return true return true
} }
} }
@@ -399,7 +400,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
redirURL, err := url.ParseRequestURI(redir) redirURL, err := url.ParseRequestURI(redir)
if err != nil { if err != nil {
lg.Error("invalid redirect", "err", err) lg.Error("invalid redirect", "err", err)
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), makeCode(err), http.StatusBadRequest) s.respondWithStatus(w, r, localizer.T("invalid_redirect"), http.StatusBadRequest)
return return
} }
@@ -408,7 +409,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
// allowed // allowed
default: default:
lg.Error("XSS attempt blocked, invalid redirect scheme", "scheme", redirURL.Scheme) lg.Error("XSS attempt blocked, invalid redirect scheme", "scheme", redirURL.Scheme)
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest) s.respondWithStatus(w, r, localizer.T("invalid_redirect"), http.StatusBadRequest)
return return
} }
@@ -422,7 +423,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host}) s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host}) s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
lg.Warn("user has cookies disabled, this is not an anubis bug") lg.Warn("user has cookies disabled, this is not an anubis bug")
s.respondWithError(w, r, localizer.T("cookies_disabled"), "") s.respondWithError(w, r, localizer.T("cookies_disabled"))
return return
} }
@@ -431,19 +432,19 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
urlParsed, err := r.URL.Parse(redir) urlParsed, err := r.URL.Parse(redir)
if err != nil { if err != nil {
s.respondWithError(w, r, localizer.T("redirect_not_parseable"), makeCode(err)) s.respondWithError(w, r, localizer.T("redirect_not_parseable"))
return return
} }
if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !matchRedirectDomain(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host { if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !matchRedirectDomain(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host {
lg.Debug("domain not allowed", "domain", urlParsed.Host) lg.Debug("domain not allowed", "domain", urlParsed.Host)
s.respondWithError(w, r, localizer.T("redirect_domain_not_allowed"), "") s.respondWithError(w, r, localizer.T("redirect_domain_not_allowed"))
return return
} }
cr, rule, err := s.check(r, lg) cr, rule, err := s.check(r, lg)
if err != nil { if err != nil {
lg.Error("check failed", "err", err) lg.Error("check failed", "err", err)
s.respondWithError(w, r, fmt.Sprintf("%s \"passChallenge\"", localizer.T("internal_server_error")), makeCode(err)) s.respondWithError(w, r, fmt.Sprintf("%s \"passChallenge\"", localizer.T("internal_server_error")))
return return
} }
lg = lg.With("check_result", cr) lg = lg.With("check_result", cr)
@@ -451,20 +452,20 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
chall, err := s.getChallenge(r) chall, err := s.getChallenge(r)
if err != nil { if err != nil {
lg.Error("getChallenge failed", "err", err) lg.Error("getChallenge failed", "err", err)
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm), makeCode(err)) s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
return return
} }
if chall.Spent { if chall.Spent {
lg.Error("double spend prevented", "reason", "double_spend") lg.Error("double spend prevented", "reason", "double_spend")
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), "double_spend"), "") s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), "double_spend"))
return return
} }
impl, ok := challenge.Get(chall.Method) impl, ok := challenge.Get(chall.Method)
if !ok { if !ok {
lg.Error("check failed", "err", err) lg.Error("check failed", "err", err)
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm), makeCode(ErrActualAnubisBug)) s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
return return
} }
@@ -487,11 +488,11 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
switch { switch {
case errors.Is(err, challenge.ErrFailed): case errors.Is(err, challenge.ErrFailed):
lg.Error("challenge failed", "err", err) lg.Error("challenge failed", "err", err)
s.respondWithStatus(w, r, cerr.PublicReason, makeCode(err), cerr.StatusCode) s.respondWithStatus(w, r, cerr.PublicReason, cerr.StatusCode)
return return
case errors.Is(err, challenge.ErrInvalidFormat), errors.Is(err, challenge.ErrMissingField): case errors.Is(err, challenge.ErrInvalidFormat), errors.Is(err, challenge.ErrMissingField):
lg.Error("invalid challenge format", "err", err) lg.Error("invalid challenge format", "err", err)
s.respondWithError(w, r, cerr.PublicReason, makeCode(err)) s.respondWithError(w, r, cerr.PublicReason)
return return
} }
} }
@@ -511,7 +512,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(s.opts.JWTRestrictionHeader) == "" { if r.Header.Get(s.opts.JWTRestrictionHeader) == "" {
lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.") lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.")
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host}) s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.respondWithError(w, r, "failed to sign JWT", makeCode(err)) s.respondWithError(w, r, "failed to sign JWT")
return return
} else { } else {
claims["restriction"] = internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)) claims["restriction"] = internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader))
@@ -525,7 +526,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
lg.Error("failed to sign JWT", "err", err) lg.Error("failed to sign JWT", "err", err)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host}) s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.respondWithError(w, r, localizer.T("failed_to_sign_jwt"), makeCode(err)) s.respondWithError(w, r, localizer.T("failed_to_sign_jwt"))
return return
} }

View File

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

View File

@@ -58,7 +58,7 @@ type ValidateInput struct {
type Impl interface { type Impl interface {
// Setup registers any additional routes with the Impl for assets or API routes. // Setup registers any additional routes with the Impl for assets or API routes.
Setup(mux *http.ServeMux) Setup(mux *http.ServeMux) error
// Issue a new challenge to the user, called by the Anubis. // 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) 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{} type Impl struct{}
func (i *Impl) Setup(mux *http.ServeMux) {} func (i *Impl) Setup(mux *http.ServeMux) error { return nil }
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) { 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") 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 { render, h, Fragment } from "preact";
import { useState, useEffect } from "preact/hooks"; import { useState, useEffect } from "preact/hooks";
import { g, j, r, u, x } from "./xeact.js"; import { g, j, r, u, x } from "../../../../web/lib/xeact";
import { Sha256 } from "@aws-crypto/sha256-js"; import { Sha256 } from "@aws-crypto/sha256-js";
/** @jsx h */ /** @jsx h */

View File

@@ -36,7 +36,7 @@ func init() {
type impl struct{} type impl struct{}
func (i *impl) Setup(mux *http.ServeMux) {} func (i *impl) Setup(mux *http.ServeMux) error { return nil }
func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) { 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") 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 Algorithm string
} }
func (i *Impl) Setup(mux *http.ServeMux) {} func (i *Impl) Setup(mux *http.ServeMux) error { return nil }
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) { func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
loc := localization.GetLocalizer(r) loc := localization.GetLocalizer(r)

102
lib/challenge/wasm/wasm.go Normal file
View File

@@ -0,0 +1,102 @@
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

@@ -0,0 +1,44 @@
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>
}

190
lib/challenge/wasm/wasm_templ.go generated Normal file
View File

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

View File

@@ -1,9 +1,6 @@
package lib package lib
import ( import (
"bytes"
"compress/gzip"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
@@ -20,7 +17,6 @@ import (
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/web" "github.com/TecharoHQ/anubis/web"
"github.com/TecharoHQ/anubis/xess"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
@@ -28,10 +24,6 @@ import (
var domainMatchRegexp = regexp.MustCompile(`^((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`) var domainMatchRegexp = regexp.MustCompile(`^((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
var (
ErrActualAnubisBug = errors.New("this is an actual bug in Anubis, please file an issue with the magic string 'taco bell'")
)
// matchRedirectDomain returns true if host matches any of the allowed redirect // matchRedirectDomain returns true if host matches any of the allowed redirect
// domain patterns. Patterns may contain '*' which are matched using the // domain patterns. Patterns may contain '*' which are matched using the
// internal glob matcher. Matching is case-insensitive on hostnames. // internal glob matcher. Matching is case-insensitive on hostnames.
@@ -152,46 +144,6 @@ func randomChance(n int) bool {
return rand.Intn(n) == 0 return rand.Intn(n) == 0
} }
// XXX(Xe): generated by ChatGPT
func rot13(s string) string {
rotated := make([]rune, len(s))
for i, c := range s {
switch {
case c >= 'A' && c <= 'Z':
rotated[i] = 'A' + ((c - 'A' + 13) % 26)
case c >= 'a' && c <= 'z':
rotated[i] = 'a' + ((c - 'a' + 13) % 26)
default:
rotated[i] = c
}
}
return string(rotated)
}
func makeCode(err error) string {
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
errStr := fmt.Sprintf("internal error: %v", err)
fmt.Fprintln(gzw, rot13(errStr))
if err := gzw.Close(); err != nil {
panic("can't write to gzip in ram buffer")
}
const width = 16
enc := base64.StdEncoding.EncodeToString(buf.Bytes())
var builder strings.Builder
for i := 0; i < len(enc); i += width {
end := i + width
if end > len(enc) {
end = len(enc)
}
builder.WriteString(enc[i:end])
builder.WriteByte('\n')
}
return builder.String()
}
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, rule *policy.Bot, returnHTTPStatusOnly bool) { func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, rule *policy.Bot, returnHTTPStatusOnly bool) {
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
@@ -202,7 +154,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
} else { } else {
redirectURL, err := s.constructRedirectURL(r) redirectURL, err := s.constructRedirectURL(r)
if err != nil { if err != nil {
s.respondWithStatus(w, r, err.Error(), "", http.StatusBadRequest) s.respondWithStatus(w, r, err.Error(), http.StatusBadRequest)
return return
} }
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
@@ -214,7 +166,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
lg.Error("client was given a challenge but does not in fact support gzip compression") lg.Error("client was given a challenge but does not in fact support gzip compression")
s.respondWithError(w, r, localizer.T("client_error_browser"), "") s.respondWithError(w, r, localizer.T("client_error_browser"))
return return
} }
@@ -223,7 +175,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
if err != nil { if err != nil {
lg.Error("can't get challenge", "err", err) lg.Error("can't get challenge", "err", err)
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host}) s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm), makeCode(err)) s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
return return
} }
@@ -250,7 +202,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
if !ok { if !ok {
lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm) lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm)
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host}) s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm), makeCode(err)) s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
return return
} }
@@ -265,7 +217,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
component, err := impl.Issue(w, r, lg, in) component, err := impl.Issue(w, r, lg, in)
if err != nil { if err != nil {
lg.Error("[unexpected] challenge component render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this. lg.Error("[unexpected] challenge component render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error")), makeCode(err)) s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error")))
return return
} }
@@ -316,23 +268,20 @@ func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
).ServeHTTP(w, r) ).ServeHTTP(w, r)
} }
func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, message, code string) { func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, message string) {
s.respondWithStatus(w, r, message, code, http.StatusInternalServerError) s.respondWithStatus(w, r, message, http.StatusInternalServerError)
} }
func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg, code string, status int) { func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) {
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
templ.Handler(web.Base(localizer.T("oh_noes"), web.ErrorPage(msg, s.opts.WebmasterEmail, code, localizer), s.policy.Impressum, localizer), templ.WithStatus(status)).ServeHTTP(w, r) templ.Handler(web.Base(localizer.T("oh_noes"), web.ErrorPage(msg, s.opts.WebmasterEmail, localizer), s.policy.Impressum, localizer), templ.WithStatus(status)).ServeHTTP(w, r)
} }
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+anubis.StaticPath) { if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+anubis.StaticPath) {
s.mux.ServeHTTP(w, r) s.mux.ServeHTTP(w, r)
return return
} else if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+xess.BasePrefix) {
s.mux.ServeHTTP(w, r)
return
} }
s.maybeReverseProxyOrPage(w, r) s.maybeReverseProxyOrPage(w, r)
@@ -371,7 +320,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
redir := r.FormValue("redir") redir := r.FormValue("redir")
urlParsed, err := r.URL.Parse(redir) urlParsed, err := r.URL.Parse(redir)
if err != nil { if err != nil {
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest) s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), http.StatusBadRequest)
return return
} }
@@ -383,7 +332,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
if hostNotAllowed || hostMismatch { if hostNotAllowed || hostMismatch {
lg := internal.GetRequestLogger(s.logger, r) lg := internal.GetRequestLogger(s.logger, r)
lg.Debug("domain not allowed", "domain", urlParsed.Host) lg.Debug("domain not allowed", "domain", urlParsed.Host)
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest) s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), http.StatusBadRequest)
return return
} }

View File

@@ -5,17 +5,17 @@
"protected_from": "frå", "protected_from": "frå",
"made_with": "Laga med ❤️ i 🇨🇦", "made_with": "Laga med ❤️ i 🇨🇦",
"mascot_design": "Maskotdesign av", "mascot_design": "Maskotdesign av",
"ai_companies_explanation": "Du ser dette av di administratoren av denne netstaden har sett opp Anubis for å verna tenaren mot plaga av KI-selskap som aggressivt skrapar netstader. Dette kan, og held fram med å, forårsaka driftstans for netstadene, som gjer ressursane deira utilgjengelege for alle.", "ai_companies_explanation": "Du ser dette av di administratoren av denne nettstaden har sett opp Anubis for å verne sørvaren mot plaga av KI-selskap som aggressivt skrapar nettstader. Dette kan, og held frem med å, forårsake driftstans for nettstadene, som gjer ressursane deira utilgjengelege for alle.",
"anubis_compromise": "Anubis er eit kompromiss. Anubis nøyter eit «Proof-of-Work»-skjema som liknar på Hashcash, eit liknande skjema for å filtrera bort søppel-e-post. Idéen er at i små meng kan den ytterlegare lastinga lett ignorerast, men ved storslegen skraping vert byrda større og større og gjer det å skrapa mykje meir dyrt.", "anubis_compromise": "Anubis er eit kompromiss. Anubis brukar eit «Proof-of-Work»-skjema som liknar på Hashcash, eit liknande skjema for å redusere søppel-e-post. Idéen er at ved småstilte tilfelle er den ytterlegare lastinga ignorerbar, men ved storstilt skraping samlar ho på seg fart og gjer det å skrapa mykje meir dyrt.",
"hack_purpose": "Til sjuande og sist er dette ei plasshaldarløysing slik at meir tid kan verta nøytt på å fingeravtrykkja og identifisera hovudlause netlesarar (t.d. via korleis dei attgjev skrifttypar) slik at utfordringssida for arbeidsprosessen ikkje treng å synast for brukarar som er nok legitime.", "hack_purpose": "Til sjuande og sist er dette ei plasshaldarløysing slik at meir tid kan brukast på fingeravtrykk og identifisering av hovudlause nettlesarar (t.d. via korleis dei attgjev skrifttypar) slik at utfordringssida for arbeidsprosessen ikkje treng å presenterast for brukarar som er mykje meir sannsynleg å vera legitime.",
"jshelter_note": "NB: Anubis krev bruk av moderne JavaScript-funksjonar som tillegg som JShelter slår av. Venlegast slå av JShelter eller liknande tillegg for dette domenet.", "jshelter_note": "NB: Anubis krev bruk av moderne JavaScript-funksjonar som tillegg som JShelter slår av. Venlegast slå av JShelter eller liknande tillegg for dette domenet.",
"version_info": "Denne netstaden køyrer Anubis-utgåve", "version_info": "Denne nettstaden køyrer Anubis-utgåve",
"try_again": "Prøv att", "try_again": "Prøv att",
"go_home": "Far heim", "go_home": " heim",
"contact_webmaster": "eller om du tykkjer at du ikkje burde vera blokkert, venlegast tak kontakt med administratoren på", "contact_webmaster": "eller om du synest at du ikkje burde vera blokkert, venlegast tak kontakt med administratoren på",
"connection_security": "Venlegast venta medan vi stadfester tryggleiken av tilkoplinga di.", "connection_security": "Venlegast vent medan vi stadfester tryggleiken av tilkoplinga di.",
"javascript_required": "Du lyt diverre slå på JavaScript for å koma deg forbi denne utfordringa. Dette krevst fordi KI-selskap har endra sosialkontrakten om korleis netstadsverting fungerer. Ei ikkje-JS-løysing er i gang med å verta skapt.", "javascript_required": "Du lyt diverre slå på JavaScript for å koma deg forbi denne utfordringa. Dette krevst av di KI-selskap har endra sosialkontrakten om korleis nettstadsverting fungerer. Ei ikkje-JS-løysing er i gang med å skapast.",
"benchmark_requires_js": "JavaScript må vera slegen på for å køyra samanlikningsverktøyet.", "benchmark_requires_js": "JavaScript må vera slegen på for å køyre samanlikningsverktøyet.",
"difficulty": "Vanskenivå:", "difficulty": "Vanskenivå:",
"algorithm": "Algoritme:", "algorithm": "Algoritme:",
"compare": "Jamfør:", "compare": "Jamfør:",
@@ -25,9 +25,9 @@
"iters_a": "Oppattakingar A", "iters_a": "Oppattakingar A",
"time_b": "Tid B", "time_b": "Tid B",
"iters_b": "Oppattakingar B", "iters_b": "Oppattakingar B",
"static_check_endpoint": "Dette er berre eit sjekkeendepunkt for din omvende proxy å nøyta.", "static_check_endpoint": "Dette er berre eit sjekkeendepunkt for din omvende proxy å bruke.",
"authorization_required": "Legitimering krevst", "authorization_required": "Legitimasjon krevst",
"cookies_disabled": "Netlesaren din er konfigurert for å avslå informasjonskapslar. Anubis krev informasjonskapslar for å stadfesta at du er ein ekte brukar. Venlegast slå på informasjonskapslar på dette domenet.", "cookies_disabled": "Nettlesaren din er konfigurert for å avslå informasjonskapslar. Anubis krev informasjonskapslar for å stadfeste at du er ein ekte brukar. Venlegast slå på informasjonskapslar på dette domenet.",
"access_denied": "Tilgang nekta: feilkode", "access_denied": "Tilgang nekta: feilkode",
"dronebl_entry": "DroneBL rapporterte ei oppføring.", "dronebl_entry": "DroneBL rapporterte ei oppføring.",
"see_dronebl_lookup": "sjå", "see_dronebl_lookup": "sjå",
@@ -35,32 +35,32 @@
"invalid_redirect": "Ugyldig omdirigering", "invalid_redirect": "Ugyldig omdirigering",
"redirect_not_parseable": "Omdirigerings-URL-en kunne ikkje tolkast", "redirect_not_parseable": "Omdirigerings-URL-en kunne ikkje tolkast",
"redirect_domain_not_allowed": "Omdirigeringsdomenet er ikkje tillate", "redirect_domain_not_allowed": "Omdirigeringsdomenet er ikkje tillate",
"failed_to_sign_jwt": "mislukkast i å signera JWT", "failed_to_sign_jwt": "mislukkast i å signere JWT",
"invalid_invocation": "Ugyldig framkalling av MakeChallenge", "invalid_invocation": "Ugyldig framkalling av MakeChallenge",
"client_error_browser": "Klientfeil: Venlegast stadfest at netlesaren din er oppdatert og prøv att seinare.", "client_error_browser": "Klientfeil: Venlegast stadfest at nettlesaren din er oppdatert og prøv att seinare.",
"oh_noes": "Å nei!", "oh_noes": "Å nei!",
"benchmarking_anubis": "Samanliknar Anubis!", "benchmarking_anubis": "Samanliknar Anubis!",
"you_are_not_a_bot": "Du er ikkje ein bot!", "you_are_not_a_bot": "Du er ikkje ein bot!",
"making_sure_not_bot": "Stadfester at du ikkje er ein bot!", "making_sure_not_bot": "Stadfester at du ikkje er ein bot!",
"celphase": "CELPHASE", "celphase": "CELPHASE",
"js_web_crypto_error": "Netlesaren din har ikkje eit fungerande web.crypto-element. Ser du dette med ei sikker tilkopling?", "js_web_crypto_error": "Nettlesaren din har ikkje eit fungerande web.crypto-element. Ser du dette med ei sikker tilkopling?",
"js_web_workers_error": "Netlesaren din stør ikkje netarbeidarar (Anubis nøyter dei for å undangå å frysa netlesaren din). Har du eit tillegg som JShelter installert?", "js_web_workers_error": "Nettlesaren din støttar ikkje nettarbeidarar (Anubis brukar dette for å unngå å fryse nettlesaren din). Har du eit tillegg som JShelter installert?",
"js_cookies_error": "Netlesaren lagrar ikkje informasjonskapslar. Anubis nøyter informasjonskapslar for å avgjera kva klientar har lukkast i utfordringa ved å lagra ein signert lykel i ein informasjonskapsel. Venlegast slå på informasjonskapslar på dette domenet. Namna på informasjonskapslane Anubis lagrar, kan ymsa utan varsel. Informasjonskapselnamn og -verdiar er ikkje ein del av det offentlege API-et.", "js_cookies_error": "Nettlesaren lagrar ikkje informasjonskapslar. Anubis brukar informasjonskapslar for å avgjera kva klientar har lukkast i utfordringa ved å lagra ein signert token i ein informasjonskapsel. Venlegast slå på informasjonskapslar på dette domenet. Namna på informasjonskapslane Anubis lagrar, kan variere utan varsel. Informasjonskapselnamn og -verdiar er ikkje ein del av det offentlege API-et.",
"js_context_not_secure": "Du nøyter ikkje ei sikker tilkopling!", "js_context_not_secure": "Du brukar ikkje ei sikker tilkopling!",
"js_context_not_secure_msg": "Prøv å kopla til over HTTPS eller fortel administratoren å oppretta HTTPS. Sjå <a hreflang=\"en\" href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a> for fleire opplysingar.", "js_context_not_secure_msg": "Prøv å kople til over HTTPS eller fortel administratoren å opprette HTTPS. Sjå <a hreflang=\"en\" href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a> for fleire opplysingar.",
"js_calculating": "Reknar…", "js_calculating": "Reknar…",
"js_missing_feature": "Manglar funksjon", "js_missing_feature": "Manglar funksjon",
"js_challenge_error": "Utfordringsfeil!", "js_challenge_error": "Utfordringsfeil!",
"js_challenge_error_msg": "Mislukkast i å tolka sjekkalgoritmen. Du burde lasta inn denne sida på nytt.", "js_challenge_error_msg": "Mislukkast i å tolke sjekkalgoritmen. Du burde laste inn denne sida på nytt.",
"js_calculating_difficulty": "Reknar…<br/>Vanskenivå:", "js_calculating_difficulty": "Reknar…<br/>Vanskenivå:",
"js_speed": "fart:", "js_speed": "fart:",
"js_verification_longer": "Verifisering tek lenger enn venta. Venlegast ikkje last inn denne sida på nytt.", "js_verification_longer": "Verifisering tek lengre enn forventa. Venlegast ikkje last inn denne sida på nytt.",
"js_success": "Vellukka!", "js_success": "Vellykka!",
"js_done_took": "Ferdig! Tok", "js_done_took": "Ferdig! Tok",
"js_iterations": "oppattakingar", "js_iterations": "oppattakingar",
"js_finished_reading": "Eg har slutta å lesa, hald fram →", "js_finished_reading": "Eg har slutta å lesa, hald fram →",
"js_calculation_error": "Rekningsfeil!", "js_calculation_error": "Rekningsfeil!",
"js_calculation_error_msg": "Mislukkast i å rekna utfordring:", "js_calculation_error_msg": "Mislukkast i å rekne utfordring:",
"missing_required_forwarded_headers": "Vantande naudsynte «X-Forwarded-*»-overskrifter", "missing_required_forwarded_headers": "Manglende nødvendige X-Forwarded-* headers",
"simplified_explanation": "Dette er eit tiltak mot robotar og ondsinna førespurnader som liknar på ein CAPTCHA. Men i staden for å måtte gjera arbeidet sjølv, får netlesaren din ei utrekningsoppgåve som han må løysa for å stadfesta at han er ein gyldig klient. Dette konseptet vert kalla <a href=\"https://en.wikipedia.org/wiki/Proof_of_work\">arbeidsstadfesting</a>. Oppgåva vert rekna ut på nokre få sekund, og du får tilgang til nettstaden. Takk for forståinga di og tolmodet ditt." "simplified_explanation": "Dette er eit tiltak mot robotar og vondsinna førespurnader som liknar på ein CAPTCHA. Men i staden for å måtte gjere arbeidet sjølv, får nettlesaren din ei utrekningsoppgåve som han må løyse for å sikre at han er ein gyldig klient. Dette konseptet blir kalla <a href=\"https://en.wikipedia.org/wiki/Proof_of_work\">Arbeidsbevis</a>. Oppgåva blir rekna ut på nokre få sekund, og du får tilgang til nettstaden. Takk for di forståing og tålmod."
} }

View File

@@ -48,7 +48,7 @@ func (Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface
go result.cleanupThread(ctx) go result.cleanupThread(ctx)
return result, nil return store.NewActorifiedStore(result), nil
} }
// Valid parses and validates the bbolt store Config or returns // Valid parses and validates the bbolt store Config or returns

20
package-lock.json generated
View File

@@ -1,16 +1,18 @@
{ {
"name": "@techaro/anubis", "name": "@techaro/anubis",
"version": "1.23.0-pre1", "version": "1.22.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@techaro/anubis", "name": "@techaro/anubis",
"version": "1.23.0-pre1", "version": "1.22.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/sha256-js": "^5.2.0",
"preact": "^10.27.2" "@haribala/wasm2js": "^1.1.1",
"preact": "^10.27.2",
"wasm-feature-detect": "^1.8.0"
}, },
"devDependencies": { "devDependencies": {
"cssnano": "^7.1.1", "cssnano": "^7.1.1",
@@ -503,6 +505,12 @@
"node": ">=18" "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": { "node_modules/@smithy/is-array-buffer": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
@@ -2685,6 +2693,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@techaro/anubis", "name": "@techaro/anubis",
"version": "1.23.0-pre1", "version": "1.22.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -8,13 +8,20 @@
"test:integration": "npm run assets && go test -v ./internal/test", "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: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", "test:integration:docker": "npm run assets && go test -v ./internal/test --playwright-runner=docker",
"assets": "go generate ./... && ./web/build.sh && ./xess/build.sh", "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",
"build": "npm run assets && go build -o ./var/anubis ./cmd/anubis", "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", "dev": "npm run assets && go run ./cmd/anubis --use-remote-address --target http://localhost:3000",
"container": "npm run assets && go run ./cmd/containerbuild", "container": "npm run assets && go run ./cmd/containerbuild",
"package": "yeet", "package": "yeet",
"lint": "make lint" "lint": "make lint"
}, },
"imports": {
"lib/*": "./web/lib/*"
},
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
@@ -29,6 +36,8 @@
}, },
"dependencies": { "dependencies": {
"@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/sha256-js": "^5.2.0",
"preact": "^10.27.2" "@haribala/wasm2js": "^1.1.1",
"preact": "^10.27.2",
"wasm-feature-detect": "^1.8.0"
} }
} }

4
rustup-toolchain.toml Normal file
View File

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

24
scripts/build_wasm.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/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

@@ -1,82 +0,0 @@
#!/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

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

View File

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

View File

@@ -211,6 +211,8 @@ 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/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 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= 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 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=

View File

@@ -1,5 +0,0 @@
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

@@ -1,26 +0,0 @@
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 ci
npm run build npm run build
SKIP_INTEGRATION=1 go test ./... SKIP_INTEGRATION=1 go test -v ./...

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-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 Hosts["aarch64-16k"]="ci@192.168.2.28" # GOARCH=arm64 GOOS=linux 16k page size
CIRunnerImage="ghcr.io/techarohq/anubis/ci-runner:latest" CIRunnerImage="ghcr.io/techarohq/ci-images/ssh-ci:latest"
RunID=${GITHUB_RUN_ID:-$(uuidgen)} RunID=${GITHUB_RUN_ID:-$(uuidgen)}
RunFolder="anubis/runs/${RunID}" RunFolder="anubis/runs/${RunID}"
Target="${Hosts["$1"]}" Target="${Hosts["$1"]}"

7
wasm/anubis/Cargo.toml Normal file
View File

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

72
wasm/anubis/src/lib.rs Normal file
View File

@@ -0,0 +1,72 @@
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};

22
wasm/pow/hashx/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[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"

156
wasm/pow/hashx/src/lib.rs Normal file
View File

@@ -0,0 +1,156 @@
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

@@ -0,0 +1,21 @@
[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"

1
wasm/pow/sha256/run.html Normal file
View File

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

105
wasm/pow/sha256/run.js Normal file
View File

@@ -0,0 +1,105 @@
// 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()));

166
wasm/pow/sha256/src/lib.rs Normal file
View File

@@ -0,0 +1,166 @@
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()
}

316
wasm/wasm.go Normal file
View File

@@ -0,0 +1,316 @@
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
}

181
wasm/wasm_test.go Normal file
View File

@@ -0,0 +1,181 @@
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,6 +31,9 @@ THE SOFTWARE.
Includes code from https://github.com/aws/aws-sdk-js-crypto-helpers which is Includes code from https://github.com/aws/aws-sdk-js-crypto-helpers which is
used under the terms of the Apache 2 license. 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 @licend The above is the entire license notice
for the JavaScript code in this page. for the JavaScript code in this page.
*/' */'
@@ -49,7 +52,7 @@ for file in js/**/*.ts js/**/*.mjs; do
mkdir -p "$(dirname "$out")" mkdir -p "$(dirname "$out")"
esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE" esbuild "$file" --sourcemap --minify --bundle --outfile="$out" --banner:js="$LICENSE"
gzip -f -k -n "$out" gzip -f -k -n "$out"
zstd -f -k --ultra -22 "$out" zstd -f -k --ultra -22 "$out"
brotli -fZk "$out" brotli -fZk "$out"

View File

@@ -22,8 +22,8 @@ func BaseWithChallengeAndOGTags(title string, body templ.Component, impressum *c
}, ogTags, localizer) }, ogTags, localizer)
} }
func ErrorPage(msg, mail, code string, localizer *localization.SimpleLocalizer) templ.Component { func ErrorPage(msg, mail string, localizer *localization.SimpleLocalizer) templ.Component {
return errorPage(msg, mail, code, localizer) return errorPage(msg, mail, localizer)
} }
func Bench(localizer *localization.SimpleLocalizer) templ.Component { func Bench(localizer *localization.SimpleLocalizer) templ.Component {

View File

@@ -88,13 +88,10 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal
</html> </html>
} }
templ errorPage(message, mail, code string, localizer *localization.SimpleLocalizer) { templ errorPage(message, mail string, localizer *localization.SimpleLocalizer) {
<div class="centered-div"> <div class="centered-div">
<img id="image" alt="Sad Anubis" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }/> <img id="image" alt="Sad Anubis" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }/>
<p>{ message }.</p> <p>{ message }.</p>
if code != "" {
<code><pre>{ code }</pre></code>
}
if mail != "" { if mail != "" {
<p> <p>
<a href="/">{ localizer.T("go_home") }</a> { localizer.T("contact_webmaster") } <a href="/">{ localizer.T("go_home") }</a> { localizer.T("contact_webmaster") }

253
web/index_templ.go generated
View File

@@ -283,7 +283,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
}) })
} }
func errorPage(message, mail, code string, localizer *localization.SimpleLocalizer) templ.Component { func errorPage(message, mail string, localizer *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -334,73 +334,72 @@ func errorPage(message, mail, code string, localizer *localization.SimpleLocaliz
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if code != "" { if mail != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<code><pre>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<p><a href=\"/\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(code) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 96, Col: 20} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 97, Col: 40}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</pre></code> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if mail != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p><a href=\"/\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var20 string var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home")) templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("contact_webmaster"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 100, Col: 40} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 97, Col: 81}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " <a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 string var templ_7745c5c3_Var21 templ.SafeURL
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("contact_webmaster")) templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs("mailto:" + templ.SafeURL(mail))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 100, Col: 81} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 98, Col: 45}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " <a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var22 templ.SafeURL var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs("mailto:" + templ.SafeURL(mail)) templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(mail)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 101, Col: 45} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 99, Col: 11}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p><a href=\"/\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(mail) templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 102, Col: 11} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 103, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -410,26 +409,8 @@ func errorPage(message, mail, code string, localizer *localization.SimpleLocaliz
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<p><a href=\"/\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 106, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -453,39 +434,39 @@ func StaticHappy(localizer *localization.SimpleLocalizer) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var25 := templ.GetChildren(ctx) templ_7745c5c3_Var24 := templ.GetChildren(ctx)
if templ_7745c5c3_Var25 == nil { if templ_7745c5c3_Var24 == nil {
templ_7745c5c3_Var25 = templ.NopComponent templ_7745c5c3_Var24 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"centered-div\"><img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"centered-div\"><img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs("/.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: `index.templ`, Line: 114, Col: 18}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"><p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("static_check_endpoint"))
anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 117, Col: 18} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 116, Col: 43}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\"><p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("static_check_endpoint"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 119, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -509,181 +490,181 @@ func bench(localizer *localization.SimpleLocalizer) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var28 := templ.GetChildren(ctx) templ_7745c5c3_Var27 := templ.GetChildren(ctx)
if templ_7745c5c3_Var28 == nil { if templ_7745c5c3_Var27 == nil {
templ_7745c5c3_Var28 = templ.NopComponent templ_7745c5c3_Var27 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 127, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</th><th style=\"width:4rem\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time")) templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 130, Col: 51} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 128, Col: 50}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_a"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 131, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</th><th style=\"width:4rem\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</th><th style=\"width:4rem\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 131, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_a")) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_a"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 134, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 132, Col: 52}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</th><th style=\"width:4.5rem\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_b"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 133, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</th><th style=\"width:4rem\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</th><th style=\"width:4rem\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_a"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 135, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</th><th style=\"width:4.5rem\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_b")) templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_b"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 136, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 134, Col: 52}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</th><th style=\"width:4rem\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var34 string var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_b")) templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 137, Col: 52} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 143, Col: 166}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><p id=\"status\" style=\"max-width:256px\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var35 string var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 146, Col: 166} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 144, Col: 66}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"><p id=\"status\" style=\"max-width:256px\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</p><script async type=\"module\" src=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var36 string var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading")) templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 147, Col: 66} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 145, Col: 138}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</p><script async type=\"module\" src=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"></script><div id=\"sparkline\"></div><noscript><p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var37 string var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version) templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("benchmark_requires_js"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 148, Col: 138} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 148, Col: 45}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"></script><div id=\"sparkline\"></div><noscript><p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var38 string var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("benchmark_requires_js")) templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("difficulty"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 151, Col: 45} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 154, Col: 88}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var39 string var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("difficulty")) templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("algorithm"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 157, Col: 88} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 158, Col: 87}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var40 string var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("algorithm")) templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("compare"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 161, Col: 87} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 162, Col: 83}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("compare"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 165, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,118 @@
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

@@ -0,0 +1,23 @@
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 { try {
const t0 = Date.now(); const t0 = Date.now();
const { hash, nonce } = await process( const { hash, nonce } = await process(
{ basePrefix, version: anubisVersion }, { basePrefix, version: anubisVersion, algorithm: rules.algorithm },
challenge.randomData, challenge.randomData,
rules.difficulty, rules.difficulty,
null, null,

105
web/js/worker/wasm.ts Normal file
View File

@@ -0,0 +1,105 @@
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,
});
});

106
web/js/worker/wasm2js.ts Normal file
View File

@@ -0,0 +1,106 @@
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,
});
});

2
web/static/wasm/.gitignore vendored Normal file
View File

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

View File

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