mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-05 16:28:17 +00:00
Compare commits
27 Commits
release/v1
...
Xe/minimiz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43b8a9257a | ||
|
|
661d72474b | ||
|
|
2b28439137 | ||
|
|
08bb7f953c | ||
|
|
b4a2e1a6a0 | ||
|
|
28828a2e93 | ||
|
|
feca1ddeea | ||
|
|
eab62f7611 | ||
|
|
c896c63a0b | ||
|
|
f9f5430dac | ||
|
|
5a07684f99 | ||
|
|
4bc00e5a65 | ||
|
|
5237291072 | ||
|
|
0f41388bd7 | ||
|
|
052316ba25 | ||
|
|
db5143ae7a | ||
|
|
3771a3b627 | ||
|
|
3683f95933 | ||
|
|
168329fff0 | ||
|
|
52ca5390c2 | ||
|
|
6b2ae30bae | ||
|
|
937f1dd330 | ||
|
|
bb4f49cfd9 | ||
|
|
38d62eeb56 | ||
|
|
57c3e9f1b2 | ||
|
|
e9a6ebffbb | ||
|
|
a3c026977f |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
web/index_templ.go linguist-generated
|
||||||
31
.github/workflows/docker-pr.yml
vendored
31
.github/workflows/docker-pr.yml
vendored
@@ -20,11 +20,29 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- name: Set up Homebrew
|
||||||
with:
|
uses: Homebrew/actions/setup-homebrew@master
|
||||||
go-version: '1.24.x'
|
|
||||||
|
|
||||||
- uses: ko-build/setup-ko@v0.8
|
- name: Setup Homebrew cellar cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/home/linuxbrew/.linuxbrew/Cellar
|
||||||
|
/home/linuxbrew/.linuxbrew/bin
|
||||||
|
/home/linuxbrew/.linuxbrew/etc
|
||||||
|
/home/linuxbrew/.linuxbrew/include
|
||||||
|
/home/linuxbrew/.linuxbrew/lib
|
||||||
|
/home/linuxbrew/.linuxbrew/opt
|
||||||
|
/home/linuxbrew/.linuxbrew/sbin
|
||||||
|
/home/linuxbrew/.linuxbrew/share
|
||||||
|
/home/linuxbrew/.linuxbrew/var
|
||||||
|
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-homebrew-cellar-
|
||||||
|
|
||||||
|
- name: Install Brew dependencies
|
||||||
|
run: |
|
||||||
|
brew bundle
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -35,9 +53,12 @@ jobs:
|
|||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
go run ./cmd/containerbuild --docker-repo ghcr.io/techarohq/anubis --slog-level debug
|
npm ci
|
||||||
|
npm run container
|
||||||
env:
|
env:
|
||||||
PULL_REQUEST_ID: ${{ github.event.number }}
|
PULL_REQUEST_ID: ${{ github.event.number }}
|
||||||
|
DOCKER_REPO: ghcr.io/techarohq/anubis
|
||||||
|
SLOG_LEVEL: debug
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
echo "Test this with:"
|
echo "Test this with:"
|
||||||
|
|||||||
33
.github/workflows/docker.yml
vendored
33
.github/workflows/docker.yml
vendored
@@ -26,11 +26,29 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- name: Set up Homebrew
|
||||||
with:
|
uses: Homebrew/actions/setup-homebrew@master
|
||||||
go-version: '1.24.x'
|
|
||||||
|
|
||||||
- uses: ko-build/setup-ko@v0.8
|
- name: Setup Homebrew cellar cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/home/linuxbrew/.linuxbrew/Cellar
|
||||||
|
/home/linuxbrew/.linuxbrew/bin
|
||||||
|
/home/linuxbrew/.linuxbrew/etc
|
||||||
|
/home/linuxbrew/.linuxbrew/include
|
||||||
|
/home/linuxbrew/.linuxbrew/lib
|
||||||
|
/home/linuxbrew/.linuxbrew/opt
|
||||||
|
/home/linuxbrew/.linuxbrew/sbin
|
||||||
|
/home/linuxbrew/.linuxbrew/share
|
||||||
|
/home/linuxbrew/.linuxbrew/var
|
||||||
|
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-homebrew-cellar-
|
||||||
|
|
||||||
|
- name: Install Brew dependencies
|
||||||
|
run: |
|
||||||
|
brew bundle
|
||||||
|
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -48,11 +66,14 @@ jobs:
|
|||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
go run ./cmd/containerbuild --docker-repo ghcr.io/techarohq/anubis --slog-level debug
|
npm ci
|
||||||
|
npm run container
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: ghcr.io/techarohq/anubis
|
||||||
|
SLOG_LEVEL: debug
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
if: ${{github.event_name == 'pull_request'}}
|
|
||||||
with:
|
with:
|
||||||
subject-name: ghcr.io/techarohq/anubis
|
subject-name: ghcr.io/techarohq/anubis
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
subject-digest: ${{ steps.build.outputs.digest }}
|
||||||
|
|||||||
15
.github/workflows/go.yml
vendored
15
.github/workflows/go.yml
vendored
@@ -11,7 +11,7 @@ permissions:
|
|||||||
actions: write
|
actions: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
go_tests:
|
||||||
#runs-on: alrest-techarohq
|
#runs-on: alrest-techarohq
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -67,10 +67,19 @@ jobs:
|
|||||||
- name: install playwright browsers
|
- name: install playwright browsers
|
||||||
run: |
|
run: |
|
||||||
npx --yes playwright@1.50.1 install --with-deps
|
npx --yes playwright@1.50.1 install --with-deps
|
||||||
npx --yes playwright@1.50.1 run-server --port 3000 &
|
npx --yes playwright@1.50.1 run-server --port 9001 &
|
||||||
|
|
||||||
|
- name: install node deps
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run assets
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build ./...
|
run: go build ./...
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v ./...
|
run: npm run test
|
||||||
|
|
||||||
|
- uses: dominikh/staticcheck-action@v1
|
||||||
|
with:
|
||||||
|
version: "latest"
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,12 @@
|
|||||||
.env
|
.env
|
||||||
|
*.deb
|
||||||
*.rpm
|
*.rpm
|
||||||
|
|
||||||
# Go binaries and test artifacts
|
# Go binaries and test artifacts
|
||||||
main
|
main
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_store
|
||||||
5
Brewfile
5
Brewfile
@@ -1,4 +1,7 @@
|
|||||||
# programming languages
|
# programming languages
|
||||||
brew "go@1.24"
|
brew "go@1.24"
|
||||||
brew "node"
|
brew "node"
|
||||||
brew "ko"
|
brew "ko"
|
||||||
|
brew "esbuild"
|
||||||
|
brew "zstd"
|
||||||
|
brew "brotli"
|
||||||
23
Dockerfile
23
Dockerfile
@@ -1,23 +0,0 @@
|
|||||||
FROM docker.io/library/golang:1.24 AS build
|
|
||||||
ARG BUILDKIT_SBOM_SCAN_CONTEXT=true BUILDKIT_SBOM_SCAN_STAGE=true
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY go.mod go.sum /app/
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN --mount=type=cache,target=/root/.cache \
|
|
||||||
VERSION=$(git describe --tags --always --dirty) \
|
|
||||||
&& go build -o /app/bin/anubis -ldflags="-X github.com/TecharoHQ/anubis.Version=${VERSION}" ./cmd/anubis
|
|
||||||
|
|
||||||
FROM docker.io/library/debian:bookworm AS runtime
|
|
||||||
ARG BUILDKIT_SBOM_SCAN_STAGE=true
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get -y install ca-certificates
|
|
||||||
|
|
||||||
COPY --from=build /app/bin/anubis /app/bin/anubis
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/app/bin/anubis", "--healthcheck"]
|
|
||||||
CMD ["/app/bin/anubis"]
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
<!-- delete me and describe your change here -->
|
<!--
|
||||||
|
delete me and describe your change here, give enough context for a maintainer to understand what and why
|
||||||
|
|
||||||
|
See https://anubis.techaro.lol/docs/developer/code-quality for more information
|
||||||
|
-->
|
||||||
|
|
||||||
Checklist:
|
Checklist:
|
||||||
|
|
||||||
- [ ] Added a description of the changes to the `[Unreleased]` section of docs/docs/CHANGELOG.md
|
- [ ] Added a description of the changes to the `[Unreleased]` section of docs/docs/CHANGELOG.md
|
||||||
- [ ] Tested this at least manually
|
- [ ] Added test cases to [the relevant parts of the codebase](https://anubis.techaro.lol/docs/developer/code-quality)
|
||||||
|
- [ ] Ran integration tests `npm run test:integration` (unsupported on Windows, please use WSL)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ If you want to try this out, connect to [anubis.techaro.lol](https://anubis.tech
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
If you run into any issues running Anubis, please [open an issue](https://github.com/TecharoHQ/anubis/issues/new?template=Blank+issue) and tag it with the Anubis tag. Please include all the information I would need to diagnose your issue.
|
If you run into any issues running Anubis, please [open an issue](https://github.com/TecharoHQ/anubis/issues/new?template=Blank+issue). Please include all the information I would need to diagnose your issue.
|
||||||
|
|
||||||
For live chat, please join the [Patreon](https://patreon.com/cadey) and ask in the Patron discord in the channel `#anubis`.
|
For live chat, please join the [Patreon](https://patreon.com/cadey) and ask in the Patron discord in the channel `#anubis`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -24,28 +26,30 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis"
|
"github.com/TecharoHQ/anubis"
|
||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
libanubis "github.com/TecharoHQ/anubis/lib"
|
libanubis "github.com/TecharoHQ/anubis/lib"
|
||||||
|
botPolicy "github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
"github.com/TecharoHQ/anubis/web"
|
|
||||||
"github.com/facebookgo/flagenv"
|
"github.com/facebookgo/flagenv"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bind = flag.String("bind", ":8923", "network address to bind HTTP to")
|
bind = flag.String("bind", ":8923", "network address to bind HTTP to")
|
||||||
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
||||||
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
||||||
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
|
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
|
||||||
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
||||||
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
||||||
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
||||||
metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to")
|
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
||||||
socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.")
|
metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to")
|
||||||
robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots")
|
socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.")
|
||||||
policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)")
|
robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots")
|
||||||
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)")
|
||||||
target = flag.String("target", "http://localhost:3923", "target to reverse proxy to")
|
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
||||||
healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
|
target = flag.String("target", "http://localhost:3923", "target to reverse proxy to")
|
||||||
debugXRealIPDefault = flag.String("debug-x-real-ip-default", "", "If set, replace empty X-Real-Ip headers with this value, useful only for debugging Anubis and running it locally")
|
healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
|
||||||
|
useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal")
|
||||||
|
debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate")
|
||||||
)
|
)
|
||||||
|
|
||||||
func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
||||||
@@ -81,7 +85,11 @@ func setupListener(network string, address string) (net.Listener, string) {
|
|||||||
case "unix":
|
case "unix":
|
||||||
formattedAddress = "unix:" + address
|
formattedAddress = "unix:" + address
|
||||||
case "tcp":
|
case "tcp":
|
||||||
formattedAddress = "http://localhost" + address
|
if strings.HasPrefix(address, ":") { // assume it's just a port e.g. :4259
|
||||||
|
formattedAddress = "http://localhost" + address
|
||||||
|
} else {
|
||||||
|
formattedAddress = "http://" + address
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
|
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
|
||||||
}
|
}
|
||||||
@@ -137,6 +145,20 @@ func makeReverseProxy(target string) (http.Handler, error) {
|
|||||||
return rp, nil
|
return rp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startDecayMapCleanup(ctx context.Context, s *libanubis.Server) {
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
s.CleanupDecayMap()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flagenv.Parse()
|
flagenv.Parse()
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -175,19 +197,41 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
// replace the bot policy rules with a single rule that always benchmarks
|
||||||
|
if *debugBenchmarkJS {
|
||||||
|
userAgent := regexp.MustCompile(".")
|
||||||
|
policy.Bots = []botPolicy.Bot{{
|
||||||
|
Name: "",
|
||||||
|
UserAgent: userAgent,
|
||||||
|
Action: config.RuleBenchmark,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
var priv ed25519.PrivateKey
|
var priv ed25519.PrivateKey
|
||||||
if *ed25519PrivateKeyHex == "" {
|
if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
|
||||||
|
log.Fatal("do not specify both ED25519_PRIVATE_KEY_HEX and ED25519_PRIVATE_KEY_HEX_FILE")
|
||||||
|
} else if *ed25519PrivateKeyHex != "" {
|
||||||
|
priv, err = keyFromHex(*ed25519PrivateKeyHex)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err)
|
||||||
|
}
|
||||||
|
} else if *ed25519PrivateKeyHexFile != "" {
|
||||||
|
hex, err := os.ReadFile(*ed25519PrivateKeyHexFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err = keyFromHex(string(bytes.TrimSpace(hex)))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to generate ed25519 key: %v", err)
|
log.Fatalf("failed to generate ed25519 key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Warn("generating random key, Anubis will have strange behavior when multiple instances are behind the same load balancer target, for more information: see https://anubis.techaro.lol/docs/admin/installation#key-generation")
|
slog.Warn("generating random key, Anubis will have strange behavior when multiple instances are behind the same load balancer target, for more information: see https://anubis.techaro.lol/docs/admin/installation#key-generation")
|
||||||
} else {
|
|
||||||
priv, err = keyFromHex(*ed25519PrivateKeyHex)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := libanubis.New(libanubis.Options{
|
s, err := libanubis.New(libanubis.Options{
|
||||||
@@ -212,21 +256,24 @@ func main() {
|
|||||||
go metricsServer(ctx, wg.Done)
|
go metricsServer(ctx, wg.Done)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go startDecayMapCleanup(ctx, s)
|
||||||
|
|
||||||
var h http.Handler
|
var h http.Handler
|
||||||
h = s
|
h = s
|
||||||
h = internal.DefaultXRealIP(*debugXRealIPDefault, h)
|
h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
|
||||||
h = internal.XForwardedForToXRealIP(h)
|
h = internal.XForwardedForToXRealIP(h)
|
||||||
|
|
||||||
srv := http.Server{Handler: h}
|
srv := http.Server{Handler: h}
|
||||||
listener, url := setupListener(*bindNetwork, *bind)
|
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
||||||
slog.Info(
|
slog.Info(
|
||||||
"listening",
|
"listening",
|
||||||
"url", url,
|
"url", listenerUrl,
|
||||||
"difficulty", *challengeDifficulty,
|
"difficulty", *challengeDifficulty,
|
||||||
"serveRobotsTXT", *robotsTxt,
|
"serveRobotsTXT", *robotsTxt,
|
||||||
"target", *target,
|
"target", *target,
|
||||||
"version", anubis.Version,
|
"version", anubis.Version,
|
||||||
"debug-x-real-ip-default", *debugXRealIPDefault,
|
"use-remote-address", *useRemoteAddress,
|
||||||
|
"debug-benchmark-js", *debugBenchmarkJS,
|
||||||
)
|
)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -267,24 +314,3 @@ func metricsServer(ctx context.Context, done func()) {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveMainJSWithBestEncoding(w http.ResponseWriter, r *http.Request) {
|
|
||||||
priorityList := []string{"zstd", "br", "gzip"}
|
|
||||||
enc2ext := map[string]string{
|
|
||||||
"zstd": "zst",
|
|
||||||
"br": "br",
|
|
||||||
"gzip": "gz",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, enc := range priorityList {
|
|
||||||
if strings.Contains(r.Header.Get("Accept-Encoding"), enc) {
|
|
||||||
w.Header().Set("Content-Type", "text/javascript")
|
|
||||||
w.Header().Set("Content-Encoding", enc)
|
|
||||||
http.ServeFileFS(w, r, web.Static, "static/js/main.mjs."+enc2ext[enc])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/javascript")
|
|
||||||
http.ServeFileFS(w, r, web.Static, "static/js/main.mjs")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ var (
|
|||||||
dockerLabels = flag.String("docker-labels", os.Getenv("DOCKER_METADATA_OUTPUT_LABELS"), "Docker image labels")
|
dockerLabels = flag.String("docker-labels", os.Getenv("DOCKER_METADATA_OUTPUT_LABELS"), "Docker image labels")
|
||||||
dockerRepo = flag.String("docker-repo", "registry.int.xeserv.us/techaro/anubis", "Docker image repository for Anubis")
|
dockerRepo = flag.String("docker-repo", "registry.int.xeserv.us/techaro/anubis", "Docker image repository for Anubis")
|
||||||
dockerTags = flag.String("docker-tags", os.Getenv("DOCKER_METADATA_OUTPUT_TAGS"), "newline separated docker tags including the registry name")
|
dockerTags = flag.String("docker-tags", os.Getenv("DOCKER_METADATA_OUTPUT_TAGS"), "newline separated docker tags including the registry name")
|
||||||
githubActor = flag.String("github-actor", "", "GitHub actor")
|
|
||||||
githubEventName = flag.String("github-event-name", "", "GitHub event name")
|
githubEventName = flag.String("github-event-name", "", "GitHub event name")
|
||||||
pullRequestID = flag.Int("pull-request-id", -1, "GitHub pull request ID")
|
pullRequestID = flag.Int("pull-request-id", -1, "GitHub pull request ID")
|
||||||
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
||||||
@@ -31,7 +30,7 @@ func main() {
|
|||||||
|
|
||||||
internal.InitSlog(*slogLevel)
|
internal.InitSlog(*slogLevel)
|
||||||
|
|
||||||
koDockerRepo := strings.TrimRight(*dockerRepo, "/"+filepath.Base(*dockerRepo))
|
koDockerRepo := strings.TrimSuffix(*dockerRepo, "/"+filepath.Base(*dockerRepo))
|
||||||
|
|
||||||
if *githubEventName == "pull_request" && *pullRequestID != -1 {
|
if *githubEventName == "pull_request" && *pullRequestID != -1 {
|
||||||
*dockerRepo = fmt.Sprintf("ttl.sh/techaro/pr-%d/anubis", *pullRequestID)
|
*dockerRepo = fmt.Sprintf("ttl.sh/techaro/pr-%d/anubis", *pullRequestID)
|
||||||
@@ -113,11 +112,6 @@ type image struct {
|
|||||||
tag string
|
tag string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newlineSep2Comma(inp string) string {
|
|
||||||
lines := strings.Split(inp, "\n")
|
|
||||||
return strings.Join(lines, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseImageList(imageList string) ([]image, error) {
|
func parseImageList(imageList string) ([]image, error) {
|
||||||
images := strings.Split(imageList, "\n")
|
images := strings.Split(imageList, "\n")
|
||||||
var result []image
|
var result []image
|
||||||
|
|||||||
@@ -394,5 +394,5 @@
|
|||||||
"action": "CHALLENGE"
|
"action": "CHALLENGE"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dnsbl": true
|
"dnsbl": false
|
||||||
}
|
}
|
||||||
@@ -85,3 +85,23 @@ func (m *Impl[K, V]) Set(key K, value V, ttl time.Duration) {
|
|||||||
expiry: time.Now().Add(ttl),
|
expiry: time.Now().Add(ttl),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup removes all expired entries from the DecayMap.
|
||||||
|
func (m *Impl[K, V]) Cleanup() {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for key, entry := range m.data {
|
||||||
|
if now.After(entry.expiry) {
|
||||||
|
delete(m.data, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of entries in the DecayMap.
|
||||||
|
func (m *Impl[K, V]) Len() int {
|
||||||
|
m.lock.RLock()
|
||||||
|
defer m.lock.RUnlock()
|
||||||
|
return len(m.data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,3 +29,32 @@ func TestImpl(t *testing.T) {
|
|||||||
t.Error("got value even though it was supposed to be expired")
|
t.Error("got value even though it was supposed to be expired")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCleanup(t *testing.T) {
|
||||||
|
dm := New[string, string]()
|
||||||
|
|
||||||
|
dm.Set("test1", "hi1", 1*time.Second)
|
||||||
|
dm.Set("test2", "hi2", 2*time.Second)
|
||||||
|
dm.Set("test3", "hi3", 3*time.Second)
|
||||||
|
|
||||||
|
dm.expire("test1") // Force expire test1
|
||||||
|
dm.expire("test2") // Force expire test2
|
||||||
|
|
||||||
|
dm.Cleanup()
|
||||||
|
|
||||||
|
finalLen := dm.Len() // Get the length after cleanup
|
||||||
|
|
||||||
|
if finalLen != 1 { // "test3" should be the only one left
|
||||||
|
t.Errorf("Cleanup failed to remove expired entries. Expected length 1, got %d", finalLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := dm.Get("test1"); ok { // Verify Get still behaves correctly after Cleanup
|
||||||
|
t.Error("test1 should not be found after cleanup")
|
||||||
|
}
|
||||||
|
if _, ok := dm.Get("test2"); ok {
|
||||||
|
t.Error("test2 should not be found after cleanup")
|
||||||
|
}
|
||||||
|
if val, ok := dm.Get("test3"); !ok || val != "hi3" {
|
||||||
|
t.Error("test3 should still be found after cleanup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Minimize the amount of data logged when users pass, fail, or otherwise interact with Anubis
|
||||||
|
- Added a periodic cleanup routine for the decaymap that removes expired entries, ensuring stale data is properly pruned.
|
||||||
|
- Added a no-store Cache-Control header to the challenge page
|
||||||
|
- Hide the directory listings for Anubis' internal static content
|
||||||
|
- Changed `--debug-x-real-ip-default` to `--use-remote-address`, getting the IP address from the request's socket address instead.
|
||||||
|
- DroneBL lookups have been disabled by default
|
||||||
|
- Static asset builds are now done on demand instead of the results being committed to source control
|
||||||
|
- The Dockerfile has been removed as it is no longer in use
|
||||||
|
- Developer documentation has been added to the docs site
|
||||||
|
- Show more errors when some predictable challenge page errors happen ([#150](https://github.com/TecharoHQ/anubis/issues/150))
|
||||||
|
- Verification page now shows hash rate and a progress bar for completion probability.
|
||||||
|
- Added the `--debug-benchmark-js` flag for testing proof-of-work performance during development.
|
||||||
|
- Use `TrimSuffix` instead of `TrimRight` on containerbuild
|
||||||
|
- Fix the startup logs to correctly show the address and port the server is listening on
|
||||||
|
- Add [LibreJS](https://www.gnu.org/software/librejs/) banner to Anubis JavaScript to allow LibreJS users to run the challenge
|
||||||
|
- Added a wait with button continue + 30 second auto continue after 30s if you click "Why am I seeing this?"
|
||||||
|
- Fixed a typo in the challenge page title.
|
||||||
|
- Disabled running integration tests on Windows hosts due to it's reliance on posix features (see [#133](https://github.com/TecharoHQ/anubis/pull/133#issuecomment-2764732309)).
|
||||||
|
- Added support for passing the ed25519 signing key in a file with `-ed25519-private-key-hex-file` or `ED25519_PRIVATE_KEY_HEX_FILE`.
|
||||||
|
- Fixed minor typos
|
||||||
|
|
||||||
|
## v1.15.1
|
||||||
|
|
||||||
|
Zenos yae Galvus: Echo 1
|
||||||
|
|
||||||
|
Fixes a recurrence of [CVE-2025-24369](https://github.com/Xe/x/security/advisories/GHSA-56w8-8ppj-2p4f)
|
||||||
|
due to an incorrect logic change in a refactor. This allows an attacker to mint a valid
|
||||||
|
access token by passing any SHA-256 hash instead of one that matches the proof-of-work
|
||||||
|
test.
|
||||||
|
|
||||||
|
This case has been added as a regression test. It was not when CVE-2025-24369 was released
|
||||||
|
due to the project not having the maturity required to enable this kind of regression testing.
|
||||||
|
|
||||||
|
## v1.15.0
|
||||||
|
|
||||||
|
Zenos yae Galvus
|
||||||
|
|
||||||
|
> Yes...the coming days promise to be most interesting. Most interesting.
|
||||||
|
|
||||||
|
Headline changes:
|
||||||
|
|
||||||
- ed25519 signing keys for Anubis can be stored in the flag `--ed25519-private-key-hex` or envvar `ED25519_PRIVATE_KEY_HEX`; if one is not provided when Anubis starts, a new one is generated and logged
|
- ed25519 signing keys for Anubis can be stored in the flag `--ed25519-private-key-hex` or envvar `ED25519_PRIVATE_KEY_HEX`; if one is not provided when Anubis starts, a new one is generated and logged
|
||||||
|
- Add the ability to set the cookie domain with the envvar `COOKIE_DOMAIN=techaro.lol` for all domains under `techaro.lol`
|
||||||
|
- Add the ability to set the cookie partitioned flag with the envvar `COOKIE_PARTITIONED=true`
|
||||||
|
|
||||||
|
Many other small changes were made, including but not limited to:
|
||||||
|
|
||||||
- Fixed and clarified installation instructions
|
- Fixed and clarified installation instructions
|
||||||
- Introduced integration tests using Playwright
|
- Introduced integration tests using Playwright
|
||||||
- Refactor & Split up Anubis into cmd and lib.go
|
- Refactor & Split up Anubis into cmd and lib.go
|
||||||
@@ -19,10 +65,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Fix default difficulty setting that was broken in a refactor
|
- Fix default difficulty setting that was broken in a refactor
|
||||||
- Linting fixes
|
- Linting fixes
|
||||||
- Make dark mode diff lines readable in the documentation
|
- Make dark mode diff lines readable in the documentation
|
||||||
- Add the ability to set the cookie domain with the envvar `COOKIE_DOMAIN=techaro.lol` for all domains under `techaro.lol`
|
|
||||||
- Add the ability to set the cookie partitioned flag with the envvar `COOKIE_PARTITIONED=true`
|
|
||||||
- Fix CI based browser smoke test
|
- Fix CI based browser smoke test
|
||||||
|
|
||||||
|
Users running Anubis' test suite may run into issues with the integration tests on Windows hosts. This is a known issue and will be fixed at some point in the future. In the meantime, use the Windows Subsystem for Linux (WSL).
|
||||||
|
|
||||||
## v1.14.2
|
## v1.14.2
|
||||||
|
|
||||||
Livia sas Junius: Echo 2
|
Livia sas Junius: Echo 2
|
||||||
|
|||||||
@@ -41,20 +41,22 @@ Anubis has very minimal system requirements. I suspect that 128Mi of ram may be
|
|||||||
|
|
||||||
Anubis uses these environment variables for configuration:
|
Anubis uses these environment variables for configuration:
|
||||||
|
|
||||||
| Environment Variable | Default value | Explanation |
|
| Environment Variable | Default value | Explanation |
|
||||||
| :------------------------ | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :----------------------------- | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
||||||
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
||||||
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See [here](https://stackoverflow.com/a/1063760) for more information. |
|
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See [here](https://stackoverflow.com/a/1063760) for more information. |
|
||||||
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||||
| `DIFFICULTY` | `5` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
| `DIFFICULTY` | `5` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||||
| `ED25519_PRIVATE_KEY_HEX` | | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
||||||
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
|
||||||
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more 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. |
|
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
||||||
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.md). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
|
| `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. |
|
||||||
| `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. |
|
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.md). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
|
||||||
| `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. |
|
| `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. |
|
||||||
|
| `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. |
|
||||||
|
| `USE_REMOTE_ADDRESS` | unset | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead. |
|
||||||
|
|
||||||
### Key generation
|
### Key generation
|
||||||
|
|
||||||
|
|||||||
8
docs/docs/developer/_category_.json
Normal file
8
docs/docs/developer/_category_.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"label": "Developer guides",
|
||||||
|
"position": 50,
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"description": "Guides and suggestions to make Anubis development go smoothly for everyone."
|
||||||
|
}
|
||||||
|
}
|
||||||
41
docs/docs/developer/building-anubis.md
Normal file
41
docs/docs/developer/building-anubis.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
title: Building Anubis without Docker
|
||||||
|
---
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
These instructions may work, but for right now they are informative for downstream packagers more than they are ready-made instructions for administrators wanting to run Anubis on their servers.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Tools needed
|
||||||
|
|
||||||
|
In order to build a production-ready binary of Anubis, you need the following packages in your environment:
|
||||||
|
|
||||||
|
- [Go](https://go.dev) - the programming language that Anubis is written in
|
||||||
|
- [esbuild](https://esbuild.github.io/) - the JavaScript bundler Anubis uses for its production JS assets
|
||||||
|
- [Node.JS & NPM](https://nodejs.org/en) - manages some build dependencies
|
||||||
|
- `gzip` - compresses production JS (part of coreutils)
|
||||||
|
- `zstd` - compresses production JS
|
||||||
|
- `brotli` - compresses production JS
|
||||||
|
|
||||||
|
## Install dependencies
|
||||||
|
|
||||||
|
```text
|
||||||
|
go mod download
|
||||||
|
npm ci
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building static assets
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run assets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building Anubis to the `./var` folder
|
||||||
|
|
||||||
|
```text
|
||||||
|
go build -o ./var/anubis ./cmd/anubis
|
||||||
|
```
|
||||||
|
|
||||||
|
From this point it is up to you to make sure that `./var/anubis` ends up in the right place. You may want to consult the `./run` folder for useful files such as a systemd unit and `anubis.env.default` file.
|
||||||
31
docs/docs/developer/code-quality.md
Normal file
31
docs/docs/developer/code-quality.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: Code quality guidelines
|
||||||
|
---
|
||||||
|
|
||||||
|
When submitting code to Anubis, please take the time to consider the fact that this project is security software. If things go bad, bots can pummel sites into oblivion. This is not ideal for uptime.
|
||||||
|
|
||||||
|
As such, code reviews will be a bit more strict than you have seen in other projects. This is not people trying to be mean, this is a side effect of taking the problem seriously.
|
||||||
|
|
||||||
|
When making code changes, try to do the following:
|
||||||
|
|
||||||
|
- If you're submitting a bugfix, add a test case for it
|
||||||
|
- If you're changing the JavaScript, make sure the integration tests pass (`npm run test:integration`)
|
||||||
|
|
||||||
|
## Commit messages
|
||||||
|
|
||||||
|
Anubis follows the Go project's conventions for commit messages. In general, an ideal commit message should read like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
path/to/folder: brief description of the change
|
||||||
|
|
||||||
|
If the change is subtle, has implementation consequences, or is otherwise
|
||||||
|
not entirely self-describing: take the time to spell out why. If things
|
||||||
|
are very subtle, please also amend the documentation accordingly
|
||||||
|
```
|
||||||
|
|
||||||
|
The subject of a commit message should be the second half of the sentence "This commit changes the Anubis project to:". Here's a few examples:
|
||||||
|
|
||||||
|
- `disable DroneBL by default`
|
||||||
|
- `port the challenge to WebAssembly`
|
||||||
|
|
||||||
|
The extended commit message is also your place to give rationale for a new feature. When maintainers are reviewing your code, they will use this to figure out if the burden from feature maintainership is worth the merge.
|
||||||
57
docs/docs/developer/local-dev.md
Normal file
57
docs/docs/developer/local-dev.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Local development
|
||||||
|
---
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
TL;DR: `npm ci && npm run dev`
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Anubis requires the following tools to be installed to do local development:
|
||||||
|
|
||||||
|
- [Go](https://go.dev) - the programming language that Anubis is written in
|
||||||
|
- [esbuild](https://esbuild.github.io/) - the JavaScript bundler Anubis uses for its production JS assets
|
||||||
|
- [Node.JS & NPM](https://nodejs.org/en) - manages some build dependencies
|
||||||
|
- `gzip` - compresses production JS (part of coreutils)
|
||||||
|
- `zstd` - compresses production JS
|
||||||
|
- `brotli` - compresses production JS
|
||||||
|
|
||||||
|
If you have [Homebrew](https://brew.sh) installed, you can install all the dependencies with one command:
|
||||||
|
|
||||||
|
```text
|
||||||
|
brew bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't, you may need to figure out equivalents to the packages in Homebrew.
|
||||||
|
|
||||||
|
## Running Anubis locally
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Or to do it manually:
|
||||||
|
|
||||||
|
- Run `npm run assets` every time you change the CSS/JavaScript
|
||||||
|
- `go run ./cmd/anubis` with any CLI flags you want
|
||||||
|
|
||||||
|
## Building JS/CSS assets
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run assets
|
||||||
|
```
|
||||||
|
|
||||||
|
If you change the build process, make sure to update `build.sh` accordingly.
|
||||||
|
|
||||||
|
## Production-ready builds
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run container
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds a prod-ready container image with [ko](https://ko.build). If you want to change where the container image is pushed, you need to use environment variables:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DOCKER_REPO=registry.host/org/repo DOCKER_METADATA_OUTPUT_TAGS=registry.host/org/repo:latest npm run container
|
||||||
|
```
|
||||||
7
docs/docs/developer/signed-commits.md
Normal file
7
docs/docs/developer/signed-commits.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Signed commits
|
||||||
|
---
|
||||||
|
|
||||||
|
Anubis requires developers to sign their commits. This is done so that we can have a better chain of custody from contribution to owner. For more information about commit signing, [read here](https://www.freecodecamp.org/news/what-is-commit-signing-in-git/).
|
||||||
|
|
||||||
|
We do not require GPG. SSH signed commits are fine. For an overview on how to set up commit signing with your SSH key, [read here](https://dev.to/ccoveille/git-the-complete-guide-to-sign-your-commits-with-an-ssh-key-35bg).
|
||||||
@@ -2,7 +2,9 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis"
|
"github.com/TecharoHQ/anubis"
|
||||||
"github.com/sebest/xff"
|
"github.com/sebest/xff"
|
||||||
@@ -21,16 +23,29 @@ func UnchangingCache(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultXRealIP sets the X-Real-Ip header to the given value if and only if
|
// RemoteXRealIP sets the X-Real-Ip header to the request's real IP if
|
||||||
// it is not an empty string.
|
// the setting is enabled by the user.
|
||||||
func DefaultXRealIP(defaultIP string, next http.Handler) http.Handler {
|
func RemoteXRealIP(useRemoteAddress bool, bindNetwork string, next http.Handler) http.Handler {
|
||||||
if defaultIP == "" {
|
if !useRemoteAddress {
|
||||||
slog.Debug("skipping middleware, defaultIP is empty")
|
slog.Debug("skipping middleware, useRemoteAddress is empty")
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bindNetwork == "unix" {
|
||||||
|
// For local sockets there is no real remote address but the localhost
|
||||||
|
// address should be sensible.
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Header.Set("X-Real-Ip", "127.0.0.1")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Header.Set("X-Real-Ip", defaultIP)
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // this should never happen
|
||||||
|
}
|
||||||
|
r.Header.Set("X-Real-Ip", host)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -48,3 +63,23 @@ func XForwardedForToXRealIP(next http.Handler) http.Handler {
|
|||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NoStoreCache sets the Cache-Control header to no-store for the response.
|
||||||
|
func NoStoreCache(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Do not allow browsing directory listings in paths that end with /
|
||||||
|
func NoBrowsing(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
// Integration tests for Anubis, using Playwright.
|
// Integration tests for Anubis, using Playwright.
|
||||||
//
|
//
|
||||||
// These tests require an already running Anubis and Playwright server.
|
// These tests require an already running Anubis and Playwright server.
|
||||||
@@ -30,9 +32,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
serverBindAddr = flag.String("bind", "localhost:3923", "test server bind address")
|
playwrightPort = flag.Int("playwright-port", 9001, "Playwright port")
|
||||||
playwrightPort = flag.Int("playwright-port", 3000, "Playwright port")
|
playwrightServer = flag.String("playwright", "ws://localhost:9001", "Playwright server URL")
|
||||||
playwrightServer = flag.String("playwright", "ws://localhost:3000", "Playwright server URL")
|
|
||||||
playwrightMaxTime = flag.Duration("playwright-max-time", 5*time.Second, "maximum time for Playwright requests")
|
playwrightMaxTime = flag.Duration("playwright-max-time", 5*time.Second, "maximum time for Playwright requests")
|
||||||
playwrightMaxHardTime = flag.Duration("playwright-max-hard-time", 5*time.Minute, "maximum time for hard Playwright requests")
|
playwrightMaxHardTime = flag.Duration("playwright-max-hard-time", 5*time.Minute, "maximum time for hard Playwright requests")
|
||||||
|
|
||||||
@@ -221,17 +222,17 @@ func TestPlaywrightBrowser(t *testing.T) {
|
|||||||
t.Skip("skipping hard challenge with deadline")
|
t.Skip("skipping hard challenge with deadline")
|
||||||
}
|
}
|
||||||
|
|
||||||
var perfomedAction action
|
var performedAction action
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
perfomedAction, err = executeTestCase(t, tc, typ, anubisURL)
|
performedAction, err = executeTestCase(t, tc, typ, anubisURL)
|
||||||
if perfomedAction == tc.action {
|
if performedAction == tc.action {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(i+1) * 250 * time.Millisecond)
|
time.Sleep(time.Duration(i+1) * 250 * time.Millisecond)
|
||||||
}
|
}
|
||||||
if perfomedAction != tc.action {
|
if performedAction != tc.action {
|
||||||
t.Errorf("unexpected test result, expected %s, got %s", tc.action, perfomedAction)
|
t.Errorf("unexpected test result, expected %s, got %s", tc.action, performedAction)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test error: %v", err)
|
t.Fatalf("test error: %v", err)
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func New(opts Options) (*Server, error) {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
xess.Mount(mux)
|
xess.Mount(mux)
|
||||||
|
|
||||||
mux.Handle(anubis.StaticPath, internal.UnchangingCache(http.StripPrefix(anubis.StaticPath, http.FileServerFS(web.Static))))
|
mux.Handle(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(anubis.StaticPath, http.FileServerFS(web.Static)))))
|
||||||
|
|
||||||
if opts.ServeRobotsTXT {
|
if opts.ServeRobotsTXT {
|
||||||
mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -145,14 +145,13 @@ func New(opts Options) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
next http.Handler
|
next http.Handler
|
||||||
priv ed25519.PrivateKey
|
priv ed25519.PrivateKey
|
||||||
pub ed25519.PublicKey
|
pub ed25519.PublicKey
|
||||||
policy *policy.ParsedConfig
|
policy *policy.ParsedConfig
|
||||||
opts Options
|
opts Options
|
||||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||||
ChallengeDifficulty int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -162,7 +161,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
||||||
fp := sha256.Sum256(s.priv.Seed())
|
fp := sha256.Sum256(s.priv.Seed())
|
||||||
|
|
||||||
data := fmt.Sprintf(
|
challengeData := fmt.Sprintf(
|
||||||
"Accept-Language=%s,X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d",
|
"Accept-Language=%s,X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d",
|
||||||
r.Header.Get("Accept-Language"),
|
r.Header.Get("Accept-Language"),
|
||||||
r.Header.Get("X-Real-Ip"),
|
r.Header.Get("X-Real-Ip"),
|
||||||
@@ -171,18 +170,11 @@ func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
|||||||
fp,
|
fp,
|
||||||
difficulty,
|
difficulty,
|
||||||
)
|
)
|
||||||
return internal.SHA256sum(data)
|
return internal.SHA256sum(challengeData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := slog.With(
|
lg := slog.With("user_agent", r.UserAgent())
|
||||||
"user_agent", r.UserAgent(),
|
|
||||||
"accept_language", r.Header.Get("Accept-Language"),
|
|
||||||
"priority", r.Header.Get("Priority"),
|
|
||||||
"x-forwarded-for",
|
|
||||||
r.Header.Get("X-Forwarded-For"),
|
|
||||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
|
||||||
)
|
|
||||||
|
|
||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,6 +233,10 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
case config.RuleChallenge:
|
case config.RuleChallenge:
|
||||||
lg.Debug("challenge requested")
|
lg.Debug("challenge requested")
|
||||||
|
case config.RuleBenchmark:
|
||||||
|
lg.Debug("serving benchmark page")
|
||||||
|
s.RenderBench(w, r)
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||||
@@ -326,13 +322,22 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handler := internal.NoStoreCache(
|
||||||
|
templ.Handler(
|
||||||
|
web.Base("Making sure you're not a bot!", web.Index()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
|
||||||
templ.Handler(
|
templ.Handler(
|
||||||
web.Base("Making sure you're not a bot!", web.Index()),
|
web.Base("Benchmarking Anubis!", web.Bench()),
|
||||||
).ServeHTTP(w, r)
|
).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := slog.With("user_agent", r.UserAgent(), "accept_language", r.Header.Get("Accept-Language"), "priority", r.Header.Get("Priority"), "x-forwarded-for", r.Header.Get("X-Forwarded-For"), "x-real-ip", r.Header.Get("X-Real-Ip"))
|
lg := slog.With("user_agent", r.UserAgent())
|
||||||
|
|
||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -360,13 +365,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := slog.With(
|
lg := slog.With("user_agent", r.UserAgent())
|
||||||
"user_agent", r.UserAgent(),
|
|
||||||
"accept_language", r.Header.Get("Accept-Language"),
|
|
||||||
"priority", r.Header.Get("Priority"),
|
|
||||||
"x-forwarded-for", r.Header.Get("X-Forwarded-For"),
|
|
||||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
|
||||||
)
|
|
||||||
|
|
||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -428,9 +427,9 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// compare the leading zeroes
|
// compare the leading zeroes
|
||||||
if !strings.HasPrefix(response, strings.Repeat("0", s.ChallengeDifficulty)) {
|
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("difficulty check failed", "response", response, "difficulty", s.ChallengeDifficulty)
|
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
||||||
failedValidations.Inc()
|
failedValidations.Inc()
|
||||||
return
|
return
|
||||||
@@ -526,3 +525,7 @@ func (s *Server) checkRemoteAddress(b policy.Bot, addr net.IP) bool {
|
|||||||
|
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) CleanupDecayMap() {
|
||||||
|
s.DNSBLCache.Cleanup()
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,79 @@ func spawnAnubis(t *testing.T, opts Options) *Server {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type challenge struct {
|
||||||
|
Challenge string `json:"challenge"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeChallenge(t *testing.T, ts *httptest.Server) challenge {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
resp, err := ts.Client().Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't request challenge: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var chall challenge
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||||
|
t.Fatalf("can't read challenge response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chall
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression test for CVE-2025-24369
|
||||||
|
func TestCVE2025_24369(t *testing.T) {
|
||||||
|
pol := loadPolicies(t, "")
|
||||||
|
pol.DefaultDifficulty = 4
|
||||||
|
|
||||||
|
srv := spawnAnubis(t, Options{
|
||||||
|
Next: http.NewServeMux(),
|
||||||
|
Policy: pol,
|
||||||
|
|
||||||
|
CookieDomain: "local.cetacean.club",
|
||||||
|
CookiePartitioned: true,
|
||||||
|
CookieName: t.Name(),
|
||||||
|
})
|
||||||
|
|
||||||
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
chall := makeChallenge(t, ts)
|
||||||
|
calcString := fmt.Sprintf("%s%d", chall.Challenge, 0)
|
||||||
|
calculated := internal.SHA256sum(calcString)
|
||||||
|
nonce := 0
|
||||||
|
elapsedTime := 420
|
||||||
|
redir := "/"
|
||||||
|
|
||||||
|
cli := ts.Client()
|
||||||
|
cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't make request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("response", calculated)
|
||||||
|
q.Set("nonce", fmt.Sprint(nonce))
|
||||||
|
q.Set("redir", redir)
|
||||||
|
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := cli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't do challenge passing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusFound {
|
||||||
|
t.Log("Regression on CVE-2025-24369")
|
||||||
|
t.Errorf("wanted HTTP status %d, got: %d", http.StatusForbidden, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCookieSettings(t *testing.T) {
|
func TestCookieSettings(t *testing.T) {
|
||||||
pol := loadPolicies(t, "")
|
pol := loadPolicies(t, "")
|
||||||
pol.DefaultDifficulty = 0
|
pol.DefaultDifficulty = 0
|
||||||
@@ -47,7 +120,7 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
CookieName: t.Name(),
|
CookieName: t.Name(),
|
||||||
})
|
})
|
||||||
|
|
||||||
ts := httptest.NewServer(internal.DefaultXRealIP("127.0.0.1", srv))
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
cli := &http.Client{
|
cli := &http.Client{
|
||||||
@@ -72,8 +145,9 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
nonce := 0
|
nonce := 0
|
||||||
elapsedTime := 420
|
elapsedTime := 420
|
||||||
redir := "/"
|
redir := "/"
|
||||||
|
calculated := ""
|
||||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
||||||
calculated := internal.SHA256sum(calcString)
|
calculated = internal.SHA256sum(calcString)
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const (
|
|||||||
RuleAllow Rule = "ALLOW"
|
RuleAllow Rule = "ALLOW"
|
||||||
RuleDeny Rule = "DENY"
|
RuleDeny Rule = "DENY"
|
||||||
RuleChallenge Rule = "CHALLENGE"
|
RuleChallenge Rule = "CHALLENGE"
|
||||||
|
RuleBenchmark Rule = "DEBUG_BENCHMARK"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Algorithm string
|
type Algorithm string
|
||||||
@@ -80,7 +81,7 @@ func (b BotConfig) Valid() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch b.Action {
|
switch b.Action {
|
||||||
case RuleAllow, RuleChallenge, RuleDeny:
|
case RuleAllow, RuleBenchmark, RuleChallenge, RuleDeny:
|
||||||
// okay
|
// okay
|
||||||
default:
|
default:
|
||||||
errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
|
errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
|
||||||
|
|||||||
2
xess/package-lock.json → package-lock.json
generated
2
xess/package-lock.json → package-lock.json
generated
@@ -2408,4 +2408,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@techaro/anubis",
|
||||||
|
"version": "1.0.0-see-VERSION-file",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "npm run assets && go test ./...",
|
||||||
|
"test:integration": "npm run assets && go test -v ./internal/test",
|
||||||
|
"assets": "go generate ./... && ./web/build.sh && ./xess/build.sh",
|
||||||
|
"dev": "npm run assets && go run ./cmd/anubis --use-remote-address",
|
||||||
|
"container": "npm run assets && go run ./cmd/containerbuild"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"cssnano": "^7.0.6",
|
||||||
|
"cssnano-preset-advanced": "^7.0.6",
|
||||||
|
"postcss-cli": "^11.0.0",
|
||||||
|
"postcss-import": "^16.1.0",
|
||||||
|
"postcss-import-url": "^7.2.0",
|
||||||
|
"postcss-url": "^10.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
web/build.sh
Executable file
40
web/build.sh
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
LICENSE='/*
|
||||||
|
@licstart The following is the entire license notice for the
|
||||||
|
JavaScript code in this page.
|
||||||
|
|
||||||
|
Copyright (c) 2025 Xe Iaso <me@xeiaso.net>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
@licend The above is the entire license notice
|
||||||
|
for the JavaScript code in this page.
|
||||||
|
*/'
|
||||||
|
|
||||||
|
esbuild js/main.mjs --sourcemap --bundle --minify --outfile=static/js/main.mjs "--banner:js=${LICENSE}"
|
||||||
|
gzip -f -k static/js/main.mjs
|
||||||
|
zstd -f -k --ultra -22 static/js/main.mjs
|
||||||
|
brotli -fZk static/js/main.mjs
|
||||||
|
|
||||||
|
esbuild js/bench.mjs --sourcemap --bundle --minify --outfile=static/js/bench.mjs
|
||||||
@@ -3,10 +3,6 @@ package web
|
|||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
//go:generate go tool github.com/a-h/templ/cmd/templ generate
|
//go:generate go tool github.com/a-h/templ/cmd/templ generate
|
||||||
//go:generate esbuild js/main.mjs --sourcemap --bundle --minify --outfile=static/js/main.mjs
|
|
||||||
//go:generate gzip -f -k static/js/main.mjs
|
|
||||||
//go:generate zstd -f -k --ultra -22 static/js/main.mjs
|
|
||||||
//go:generate brotli -fZk static/js/main.mjs
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed static
|
//go:embed static
|
||||||
|
|||||||
@@ -13,3 +13,7 @@ func Index() templ.Component {
|
|||||||
func ErrorPage(msg string) templ.Component {
|
func ErrorPage(msg string) templ.Component {
|
||||||
return errorPage(msg)
|
return errorPage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Bench() templ.Component {
|
||||||
|
return bench()
|
||||||
|
}
|
||||||
|
|||||||
185
web/index.templ
185
web/index.templ
@@ -27,115 +27,28 @@ templ base(title string, body templ.Component) {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lds-roller,
|
#status {
|
||||||
.lds-roller div,
|
font-variant-numeric: tabular-nums;
|
||||||
.lds-roller div:after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lds-roller {
|
#progress {
|
||||||
display: inline-block;
|
display: none;
|
||||||
position: relative;
|
width: min(20rem, 90%);
|
||||||
width: 80px;
|
height: 2rem;
|
||||||
height: 80px;
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 1rem 0 2rem;
|
||||||
|
outline-color: #b16286;
|
||||||
|
outline-offset: 2px;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lds-roller div {
|
.bar-inner {
|
||||||
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
background-color: #b16286;
|
||||||
transform-origin: 40px 40px;
|
height: 100%;
|
||||||
}
|
width: 0;
|
||||||
|
transition: width 0.25s ease-in;
|
||||||
.lds-roller div:after {
|
|
||||||
content: " ";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
width: 7.2px;
|
|
||||||
height: 7.2px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
margin: -3.6px 0 0 -3.6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(1) {
|
|
||||||
animation-delay: -0.036s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(1):after {
|
|
||||||
top: 62.62742px;
|
|
||||||
left: 62.62742px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(2) {
|
|
||||||
animation-delay: -0.072s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(2):after {
|
|
||||||
top: 67.71281px;
|
|
||||||
left: 56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(3) {
|
|
||||||
animation-delay: -0.108s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(3):after {
|
|
||||||
top: 70.90963px;
|
|
||||||
left: 48.28221px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(4) {
|
|
||||||
animation-delay: -0.144s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(4):after {
|
|
||||||
top: 72px;
|
|
||||||
left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(5) {
|
|
||||||
animation-delay: -0.18s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(5):after {
|
|
||||||
top: 70.90963px;
|
|
||||||
left: 31.71779px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(6) {
|
|
||||||
animation-delay: -0.216s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(6):after {
|
|
||||||
top: 67.71281px;
|
|
||||||
left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(7) {
|
|
||||||
animation-delay: -0.252s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(7):after {
|
|
||||||
top: 62.62742px;
|
|
||||||
left: 17.37258px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(8) {
|
|
||||||
animation-delay: -0.288s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-roller div:nth-child(8):after {
|
|
||||||
top: 56px;
|
|
||||||
left: 12.28719px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes lds-roller {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@templ.JSONScript("anubis_version", anubis.Version)
|
@templ.JSONScript("anubis_version", anubis.Version)
|
||||||
@@ -176,15 +89,8 @@ templ index() {
|
|||||||
/>
|
/>
|
||||||
<p id="status">Loading...</p>
|
<p id="status">Loading...</p>
|
||||||
<script async type="module" src={ "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
|
<script async type="module" src={ "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
|
||||||
<div id="spinner" class="lds-roller">
|
<div id="progress" role="progressbar" aria-labelledby="status">
|
||||||
<div></div>
|
<div class="bar-inner"></div>
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary>Why am I seeing this?</summary>
|
<summary>Why am I seeing this?</summary>
|
||||||
@@ -215,3 +121,54 @@ templ errorPage(message string) {
|
|||||||
<p><a href="/">Go home</a></p>
|
<p><a href="/">Go home</a></p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ bench() {
|
||||||
|
<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">Time</th>
|
||||||
|
<th style="width:4rem">Iters</th>
|
||||||
|
</tr>
|
||||||
|
<tr id="table-header-compare" style="display:none">
|
||||||
|
<th style="width:4.5rem">Time A</th>
|
||||||
|
<th style="width:4rem">Iters A</th>
|
||||||
|
<th style="width:4.5rem">Time B</th>
|
||||||
|
<th style="width:4rem">Iters B</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={ "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
|
||||||
|
anubis.Version }
|
||||||
|
/>
|
||||||
|
<p id="status" style="max-width:256px">Loading...</p>
|
||||||
|
<script async type="module" src={ "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version }></script>
|
||||||
|
<div id="sparkline"></div>
|
||||||
|
<noscript>
|
||||||
|
<p>Running the benchmark tool requires JavaScript to be enabled.</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">Difficulty:</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">Algorithm:</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">Compare:</label>
|
||||||
|
<select id="compare-select" name="compare">
|
||||||
|
<option value="NONE">-</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|||||||
72
web/index_templ.go
generated
72
web/index_templ.go
generated
@@ -60,7 +60,7 @@ func base(title string, body templ.Component) templ.Component {
|
|||||||
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, 3, "\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><style>\n body,\n html {\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n margin-left: auto;\n margin-right: auto;\n }\n\n .centered-div {\n text-align: center;\n }\n\n .lds-roller,\n .lds-roller div,\n .lds-roller div:after {\n box-sizing: border-box;\n }\n\n .lds-roller {\n display: inline-block;\n position: relative;\n width: 80px;\n height: 80px;\n }\n\n .lds-roller div {\n animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;\n transform-origin: 40px 40px;\n }\n\n .lds-roller div:after {\n content: \" \";\n display: block;\n position: absolute;\n width: 7.2px;\n height: 7.2px;\n border-radius: 50%;\n background: currentColor;\n margin: -3.6px 0 0 -3.6px;\n }\n\n .lds-roller div:nth-child(1) {\n animation-delay: -0.036s;\n }\n\n .lds-roller div:nth-child(1):after {\n top: 62.62742px;\n left: 62.62742px;\n }\n\n .lds-roller div:nth-child(2) {\n animation-delay: -0.072s;\n }\n\n .lds-roller div:nth-child(2):after {\n top: 67.71281px;\n left: 56px;\n }\n\n .lds-roller div:nth-child(3) {\n animation-delay: -0.108s;\n }\n\n .lds-roller div:nth-child(3):after {\n top: 70.90963px;\n left: 48.28221px;\n }\n\n .lds-roller div:nth-child(4) {\n animation-delay: -0.144s;\n }\n\n .lds-roller div:nth-child(4):after {\n top: 72px;\n left: 40px;\n }\n\n .lds-roller div:nth-child(5) {\n animation-delay: -0.18s;\n }\n\n .lds-roller div:nth-child(5):after {\n top: 70.90963px;\n left: 31.71779px;\n }\n\n .lds-roller div:nth-child(6) {\n animation-delay: -0.216s;\n }\n\n .lds-roller div:nth-child(6):after {\n top: 67.71281px;\n left: 24px;\n }\n\n .lds-roller div:nth-child(7) {\n animation-delay: -0.252s;\n }\n\n .lds-roller div:nth-child(7):after {\n top: 62.62742px;\n left: 17.37258px;\n }\n\n .lds-roller div:nth-child(8) {\n animation-delay: -0.288s;\n }\n\n .lds-roller div:nth-child(8):after {\n top: 56px;\n left: 12.28719px;\n }\n\n @keyframes lds-roller {\n 0% {\n transform: rotate(0deg);\n }\n\n 100% {\n transform: rotate(360deg);\n }\n }\n </style>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><style>\n body,\n html {\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n margin-left: auto;\n margin-right: auto;\n }\n\n .centered-div {\n text-align: center;\n }\n\n #status {\n font-variant-numeric: tabular-nums;\n }\n\n #progress {\n display: none;\n width: min(20rem, 90%);\n height: 2rem;\n border-radius: 1rem;\n overflow: hidden;\n margin: 1rem 0 2rem;\n outline-color: #b16286;\n outline-offset: 2px;\n outline-style: solid;\n outline-width: 4px;\n }\n\n .bar-inner {\n background-color: #b16286;\n height: 100%;\n width: 0;\n transition: width 0.25s ease-in;\n }\n </style>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func base(title string, body templ.Component) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 146, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 59, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -126,7 +126,7 @@ func index() templ.Component {
|
|||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
|
||||||
anubis.Version)
|
anubis.Version)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 169, Col: 18}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 82, Col: 18}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -140,7 +140,7 @@ func index() templ.Component {
|
|||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
|
||||||
anubis.Version)
|
anubis.Version)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 175, Col: 18}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 88, Col: 18}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -153,13 +153,13 @@ func index() templ.Component {
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/main.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: 178, Col: 116}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 91, Col: 116}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
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, 10, "\"></script><div id=\"spinner\" class=\"lds-roller\"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><details><summary>Why am I seeing this?</summary><p>You are seeing this because the administrator of this website has set up <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> to protect the server against the scourge of <a href=\"https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/\">AI companies aggressively scraping websites</a>. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.</p><p>Anubis is a compromise. Anubis uses a <a href=\"https://anubis.techaro.lol/docs/design/why-proof-of-work\">Proof-of-Work</a> scheme in the vein of <a href=\"https://en.wikipedia.org/wiki/Hashcash\">Hashcash</a>, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.</p><p>Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.</p><p>Please note that Anubis requires the use of modern JavaScript features that plugins like <a href=\"https://jshelter.org/\">JShelter</a> will disable. Please disable JShelter or other such plugins for this domain.</p></details><noscript><p>Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.</p></noscript><div id=\"testarea\"></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></script><div id=\"progress\" role=\"progressbar\" aria-labelledby=\"status\"><div class=\"bar-inner\"></div></div><details><summary>Why am I seeing this?</summary><p>You are seeing this because the administrator of this website has set up <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> to protect the server against the scourge of <a href=\"https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/\">AI companies aggressively scraping websites</a>. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.</p><p>Anubis is a compromise. Anubis uses a <a href=\"https://anubis.techaro.lol/docs/design/why-proof-of-work\">Proof-of-Work</a> scheme in the vein of <a href=\"https://en.wikipedia.org/wiki/Hashcash\">Hashcash</a>, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.</p><p>Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.</p><p>Please note that Anubis requires the use of modern JavaScript features that plugins like <a href=\"https://jshelter.org/\">JShelter</a> will disable. Please disable JShelter or other such plugins for this domain.</p></details><noscript><p>Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.</p></noscript><div id=\"testarea\"></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ func errorPage(message string) templ.Component {
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/sad.webp?cacheBuster=" + anubis.Version)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/sad.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: 211, Col: 90}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 117, Col: 90}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -208,7 +208,7 @@ func errorPage(message string) templ.Component {
|
|||||||
var templ_7745c5c3_Var11 string
|
var templ_7745c5c3_Var11 string
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 213, Col: 14}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 119, Col: 14}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -222,4 +222,60 @@ func errorPage(message string) templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bench() 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_Var12 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var12 == nil {
|
||||||
|
templ_7745c5c3_Var12 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<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\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</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 {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs("/.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: `index.templ`, Line: 148, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 151, Col: 118}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</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\">Difficulty:</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\">Algorithm:</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\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|||||||
152
web/js/bench.mjs
Normal file
152
web/js/bench.mjs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import processFast from "./proof-of-work.mjs";
|
||||||
|
import processSlow from "./proof-of-work-slow.mjs";
|
||||||
|
|
||||||
|
const defaultDifficulty = 4;
|
||||||
|
const algorithms = {
|
||||||
|
fast: processFast,
|
||||||
|
slow: processSlow,
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
const difficultyInput = document.getElementById("difficulty-input");
|
||||||
|
const algorithmSelect = document.getElementById("algorithm-select");
|
||||||
|
const compareSelect = document.getElementById("compare-select");
|
||||||
|
const header = document.getElementById("table-header");
|
||||||
|
const headerCompare = document.getElementById("table-header-compare");
|
||||||
|
const results = document.getElementById("results");
|
||||||
|
|
||||||
|
const setupControls = () => {
|
||||||
|
difficultyInput.value = defaultDifficulty;
|
||||||
|
for (const alg of Object.keys(algorithms)) {
|
||||||
|
const option1 = document.createElement("option");
|
||||||
|
algorithmSelect.append(option1);
|
||||||
|
const option2 = document.createElement("option");
|
||||||
|
compareSelect.append(option2);
|
||||||
|
option1.value = option1.innerText = option2.value = option2.innerText = alg;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const benchmarkTrial = async (stats, difficulty, algorithm, signal) => {
|
||||||
|
if (!(difficulty >= 1)) {
|
||||||
|
throw new Error(`Invalid difficulty: ${difficulty}`);
|
||||||
|
}
|
||||||
|
const process = algorithms[algorithm];
|
||||||
|
if (process == null) {
|
||||||
|
throw new Error(`Unknown algorithm: ${algorithm}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawChallenge = new Uint8Array(32);
|
||||||
|
crypto.getRandomValues(rawChallenge);
|
||||||
|
const challenge = Array.from(rawChallenge)
|
||||||
|
.map((c) => c.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const t0 = performance.now();
|
||||||
|
const { hash, nonce } = await process(challenge, Number(difficulty), signal);
|
||||||
|
const t1 = performance.now();
|
||||||
|
console.log({ hash, nonce });
|
||||||
|
|
||||||
|
stats.time += t1 - t0;
|
||||||
|
stats.iters += nonce;
|
||||||
|
|
||||||
|
return { time: t1 - t0, nonce };
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = { time: 0, iters: 0 };
|
||||||
|
const comparison = { time: 0, iters: 0 };
|
||||||
|
const updateStatus = () => {
|
||||||
|
const mainRate = stats.iters / stats.time;
|
||||||
|
const compareRate = comparison.iters / comparison.time;
|
||||||
|
if (Number.isFinite(mainRate)) {
|
||||||
|
status.innerText = `Average hashrate: ${mainRate.toFixed(3)}kH/s`;
|
||||||
|
if (Number.isFinite(compareRate)) {
|
||||||
|
const change = ((mainRate - compareRate) / mainRate) * 100;
|
||||||
|
status.innerText += ` vs ${compareRate.toFixed(3)}kH/s (${change.toFixed(2)}% change)`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status.innerText = "Benchmarking...";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableCell = (text) => {
|
||||||
|
const td = document.createElement("td");
|
||||||
|
td.innerText = text;
|
||||||
|
td.style.padding = "0 0.25rem";
|
||||||
|
return td;
|
||||||
|
};
|
||||||
|
|
||||||
|
const benchmarkLoop = async (controller) => {
|
||||||
|
const difficulty = difficultyInput.value;
|
||||||
|
const algorithm = algorithmSelect.value;
|
||||||
|
const compareAlgorithm = compareSelect.value;
|
||||||
|
updateStatus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { time, nonce } = await benchmarkTrial(
|
||||||
|
stats,
|
||||||
|
difficulty,
|
||||||
|
algorithm,
|
||||||
|
controller.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.style.display = "contents";
|
||||||
|
tr.append(tableCell(`${time}ms`), tableCell(nonce));
|
||||||
|
|
||||||
|
// auto-scroll to new rows
|
||||||
|
const atBottom =
|
||||||
|
results.scrollHeight - results.clientHeight <= results.scrollTop;
|
||||||
|
results.append(tr);
|
||||||
|
if (atBottom) {
|
||||||
|
results.scrollTop = results.scrollHeight - results.clientHeight;
|
||||||
|
}
|
||||||
|
updateStatus();
|
||||||
|
|
||||||
|
if (compareAlgorithm !== "NONE") {
|
||||||
|
const { time, nonce } = await benchmarkTrial(
|
||||||
|
comparison,
|
||||||
|
difficulty,
|
||||||
|
compareAlgorithm,
|
||||||
|
controller.signal,
|
||||||
|
);
|
||||||
|
tr.append(tableCell(`${time}ms`), tableCell(nonce));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== false) {
|
||||||
|
status.innerText = e;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmarkLoop(controller);
|
||||||
|
};
|
||||||
|
|
||||||
|
let controller = null;
|
||||||
|
const reset = () => {
|
||||||
|
stats.time = stats.iters = 0;
|
||||||
|
comparison.time = comparison.iters = 0;
|
||||||
|
results.innerHTML = status.innerText = "";
|
||||||
|
|
||||||
|
const table = results.parentElement;
|
||||||
|
if (compareSelect.value !== "NONE") {
|
||||||
|
table.style.gridTemplateColumns = "repeat(4,auto)";
|
||||||
|
header.style.display = "none";
|
||||||
|
headerCompare.style.display = "contents";
|
||||||
|
} else {
|
||||||
|
table.style.gridTemplateColumns = "repeat(2,auto)";
|
||||||
|
header.style.display = "contents";
|
||||||
|
headerCompare.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller != null) {
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
|
controller = new AbortController();
|
||||||
|
benchmarkLoop(controller);
|
||||||
|
};
|
||||||
|
|
||||||
|
setupControls();
|
||||||
|
difficultyInput.addEventListener("change", reset);
|
||||||
|
algorithmSelect.addEventListener("change", reset);
|
||||||
|
compareSelect.addEventListener("change", reset);
|
||||||
|
reset();
|
||||||
252
web/js/main.mjs
252
web/js/main.mjs
@@ -5,27 +5,108 @@ import { testVideo } from "./video.mjs";
|
|||||||
const algorithms = {
|
const algorithms = {
|
||||||
"fast": processFast,
|
"fast": processFast,
|
||||||
"slow": processSlow,
|
"slow": processSlow,
|
||||||
}
|
};
|
||||||
|
|
||||||
// from Xeact
|
// from Xeact
|
||||||
const u = (url = "", params = {}) => {
|
const u = (url = "", params = {}) => {
|
||||||
let result = new URL(url, window.location.href);
|
let result = new URL(url, window.location.href);
|
||||||
Object.entries(params).forEach((kv) => {
|
Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
|
||||||
let [k, v] = kv;
|
|
||||||
result.searchParams.set(k, v);
|
|
||||||
});
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageURL = (mood, cacheBuster) =>
|
const imageURL = (mood, cacheBuster) =>
|
||||||
u(`/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });
|
u(`/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });
|
||||||
|
|
||||||
|
const dependencies = [
|
||||||
|
{
|
||||||
|
name: "WebCrypto",
|
||||||
|
msg: "Your browser doesn't have a functioning web.crypto element. Are you viewing this over a secure context?",
|
||||||
|
value: window.crypto,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Web Workers",
|
||||||
|
msg: "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?",
|
||||||
|
value: window.Worker,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function showContinueBar(hash, nonce, t0, t1) {
|
||||||
|
const barContainer = document.createElement("div");
|
||||||
|
barContainer.style.marginTop = "1rem";
|
||||||
|
barContainer.style.width = "100%";
|
||||||
|
barContainer.style.maxWidth = "32rem";
|
||||||
|
barContainer.style.background = "#3c3836";
|
||||||
|
barContainer.style.borderRadius = "4px";
|
||||||
|
barContainer.style.overflow = "hidden";
|
||||||
|
barContainer.style.cursor = "pointer";
|
||||||
|
barContainer.style.height = "2rem";
|
||||||
|
barContainer.style.marginLeft = "auto";
|
||||||
|
barContainer.style.marginRight = "auto";
|
||||||
|
barContainer.title = "Click to continue";
|
||||||
|
|
||||||
|
const barInner = document.createElement("div");
|
||||||
|
barInner.className = "bar-inner";
|
||||||
|
barInner.style.display = "flex";
|
||||||
|
barInner.style.alignItems = "center";
|
||||||
|
barInner.style.justifyContent = "center";
|
||||||
|
barInner.style.color = "white";
|
||||||
|
barInner.style.fontWeight = "bold";
|
||||||
|
barInner.style.height = "100%";
|
||||||
|
barInner.style.width = "0";
|
||||||
|
barInner.innerText = "I've finished reading, continue →";
|
||||||
|
|
||||||
|
barContainer.appendChild(barInner);
|
||||||
|
document.body.appendChild(barContainer);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
barInner.style.width = "100%";
|
||||||
|
});
|
||||||
|
|
||||||
|
barContainer.onclick = () => {
|
||||||
|
const redir = window.location.href;
|
||||||
|
window.location.replace(
|
||||||
|
u("/.within.website/x/cmd/anubis/api/pass-challenge", {
|
||||||
|
response: hash,
|
||||||
|
nonce,
|
||||||
|
redir,
|
||||||
|
elapsedTime: t1 - t0
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
const image = document.getElementById('image');
|
const image = document.getElementById('image');
|
||||||
const title = document.getElementById('title');
|
const title = document.getElementById('title');
|
||||||
const spinner = document.getElementById('spinner');
|
const progress = document.getElementById('progress');
|
||||||
const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);
|
const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);
|
||||||
|
const details = document.querySelector('details');
|
||||||
|
let userReadDetails = false;
|
||||||
|
|
||||||
|
if (details) {
|
||||||
|
details.addEventListener("toggle", () => {
|
||||||
|
if (details.open) {
|
||||||
|
userReadDetails = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ohNoes = ({ titleMsg, statusMsg, imageSrc }) => {
|
||||||
|
title.innerHTML = titleMsg;
|
||||||
|
status.innerHTML = statusMsg;
|
||||||
|
image.src = imageSrc;
|
||||||
|
progress.style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!window.isSecureContext) {
|
||||||
|
ohNoes({
|
||||||
|
titleMsg: "Your context is not secure!",
|
||||||
|
statusMsg: `Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure">MDN</a>.`,
|
||||||
|
imageSrc: imageURL("sad", anubisVersion),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// const testarea = document.getElementById('testarea');
|
// const testarea = document.getElementById('testarea');
|
||||||
|
|
||||||
@@ -36,54 +117,155 @@ const imageURL = (mood, cacheBuster) =>
|
|||||||
// title.innerHTML = "Oh no!";
|
// title.innerHTML = "Oh no!";
|
||||||
// status.innerHTML = "Checks failed. Please check your browser's settings and try again.";
|
// status.innerHTML = "Checks failed. Please check your browser's settings and try again.";
|
||||||
// image.src = imageURL("sad");
|
// image.src = imageURL("sad");
|
||||||
// spinner.innerHTML = "";
|
// progress.style.display = "none";
|
||||||
// spinner.style.display = "none";
|
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
status.innerHTML = 'Calculating...';
|
status.innerHTML = 'Calculating...';
|
||||||
|
|
||||||
|
for (const { value, name, msg } of dependencies) {
|
||||||
|
if (!value) {
|
||||||
|
ohNoes({
|
||||||
|
titleMsg: `Missing feature ${name}`,
|
||||||
|
statusMsg: msg,
|
||||||
|
imageSrc: imageURL("sad", anubisVersion),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { challenge, rules } = await fetch("/.within.website/x/cmd/anubis/api/make-challenge", { method: "POST" })
|
const { challenge, rules } = await fetch("/.within.website/x/cmd/anubis/api/make-challenge", { method: "POST" })
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (!r.ok) {
|
if (!r.ok) throw new Error("Failed to fetch config");
|
||||||
throw new Error("Failed to fetch config");
|
|
||||||
}
|
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
title.innerHTML = "Oh no!";
|
ohNoes({
|
||||||
status.innerHTML = `Failed to fetch config: ${err.message}`;
|
titleMsg: "Internal error!",
|
||||||
image.src = imageURL("sad", anubisVersion);
|
statusMsg: `Failed to fetch challenge config: ${err.message}`,
|
||||||
spinner.innerHTML = "";
|
imageSrc: imageURL("sad", anubisVersion),
|
||||||
spinner.style.display = "none";
|
});
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
const process = algorithms[rules.algorithm];
|
const process = algorithms[rules.algorithm];
|
||||||
if (!process) {
|
if (!process) {
|
||||||
title.innerHTML = "Oh no!";
|
ohNoes({
|
||||||
status.innerHTML = `Failed to resolve check algorithm. You may want to reload the page.`;
|
titleMsg: "Challenge error!",
|
||||||
image.src = imageURL("sad", anubisVersion);
|
statusMsg: `Failed to resolve check algorithm. You may want to reload the page.`,
|
||||||
spinner.innerHTML = "";
|
imageSrc: imageURL("sad", anubisVersion),
|
||||||
spinner.style.display = "none";
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
status.innerHTML = `Calculating...<br/>Difficulty: ${rules.report_as}`;
|
status.innerHTML = `Calculating...<br/>Difficulty: ${rules.report_as}, `;
|
||||||
|
progress.style.display = "inline-block";
|
||||||
|
|
||||||
|
// the whole text, including "Speed:", as a single node, because some browsers
|
||||||
|
// (Firefox mobile) present screen readers with each node as a separate piece
|
||||||
|
// of text.
|
||||||
|
const rateText = document.createTextNode("Speed: 0kH/s");
|
||||||
|
status.appendChild(rateText);
|
||||||
|
|
||||||
const t0 = Date.now();
|
let lastSpeedUpdate = 0;
|
||||||
const { hash, nonce } = await process(challenge, rules.difficulty);
|
let showingApology = false;
|
||||||
const t1 = Date.now();
|
const likelihood = Math.pow(16, -rules.report_as);
|
||||||
console.log({ hash, nonce });
|
|
||||||
|
|
||||||
title.innerHTML = "Success!";
|
try {
|
||||||
status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;
|
const t0 = Date.now();
|
||||||
image.src = imageURL("happy", anubisVersion);
|
const { hash, nonce } = await process(
|
||||||
spinner.innerHTML = "";
|
challenge,
|
||||||
spinner.style.display = "none";
|
rules.difficulty,
|
||||||
|
null,
|
||||||
|
(iters) => {
|
||||||
|
const delta = Date.now() - t0;
|
||||||
|
// only update the speed every second so it's less visually distracting
|
||||||
|
if (delta - lastSpeedUpdate > 1000) {
|
||||||
|
lastSpeedUpdate = delta;
|
||||||
|
rateText.data = `Speed: ${(iters / delta).toFixed(3)}kH/s`;
|
||||||
|
}
|
||||||
|
// the probability of still being on the page is (1 - likelihood) ^ iters.
|
||||||
|
// by definition, half of the time the progress bar only gets to half, so
|
||||||
|
// apply a polynomial ease-out function to move faster in the beginning
|
||||||
|
// and then slow down as things get increasingly unlikely. quadratic felt
|
||||||
|
// the best in testing, but this may need adjustment in the future.
|
||||||
|
|
||||||
|
const probability = Math.pow(1 - likelihood, iters);
|
||||||
|
const distance = (1 - Math.pow(probability, 2)) * 100;
|
||||||
|
progress["aria-valuenow"] = distance;
|
||||||
|
progress.firstElementChild.style.width = `${distance}%`;
|
||||||
|
|
||||||
setTimeout(() => {
|
if (probability < 0.1 && !showingApology) {
|
||||||
const redir = window.location.href;
|
status.append(
|
||||||
window.location.href = u("/.within.website/x/cmd/anubis/api/pass-challenge", { response: hash, nonce, redir, elapsedTime: t1 - t0 });
|
document.createElement("br"),
|
||||||
}, 250);
|
document.createTextNode(
|
||||||
|
"Verification is taking longer than expected. Please do not refresh the page.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
showingApology = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const t1 = Date.now();
|
||||||
|
console.log({ hash, nonce });
|
||||||
|
|
||||||
|
title.innerHTML = "Success!";
|
||||||
|
status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;
|
||||||
|
image.src = imageURL("happy", anubisVersion);
|
||||||
|
progress.style.display = "none";
|
||||||
|
|
||||||
|
if (userReadDetails) {
|
||||||
|
const container = document.getElementById("progress");
|
||||||
|
|
||||||
|
// Style progress bar as a continue button
|
||||||
|
container.style.display = "flex";
|
||||||
|
container.style.alignItems = "center";
|
||||||
|
container.style.justifyContent = "center";
|
||||||
|
container.style.height = "2rem";
|
||||||
|
container.style.borderRadius = "1rem";
|
||||||
|
container.style.cursor = "pointer";
|
||||||
|
container.style.background = "#b16286";
|
||||||
|
container.style.color = "white";
|
||||||
|
container.style.fontWeight = "bold";
|
||||||
|
container.style.outline = "4px solid #b16286";
|
||||||
|
container.style.outlineOffset = "2px";
|
||||||
|
container.style.width = "min(20rem, 90%)";
|
||||||
|
container.style.margin = "1rem auto 2rem";
|
||||||
|
container.innerHTML = "I've finished reading, continue →";
|
||||||
|
|
||||||
|
function onDetailsExpand() {
|
||||||
|
const redir = window.location.href;
|
||||||
|
window.location.replace(
|
||||||
|
u("/.within.website/x/cmd/anubis/api/pass-challenge", {
|
||||||
|
response: hash,
|
||||||
|
nonce,
|
||||||
|
redir,
|
||||||
|
elapsedTime: t1 - t0
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.onclick = onDetailsExpand;
|
||||||
|
setTimeout(onDetailsExpand, 30000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
const redir = window.location.href;
|
||||||
|
window.location.replace(
|
||||||
|
u("/.within.website/x/cmd/anubis/api/pass-challenge", {
|
||||||
|
response: hash,
|
||||||
|
nonce,
|
||||||
|
redir,
|
||||||
|
elapsedTime: t1 - t0
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
ohNoes({
|
||||||
|
titleMsg: "Calculation error!",
|
||||||
|
statusMsg: `Failed to calculate challenge: ${err.message}`,
|
||||||
|
imageSrc: imageURL("sad", anubisVersion),
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm
|
// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm
|
||||||
|
|
||||||
export default function process(data, difficulty = 5, _threads = 1) {
|
export default function process(
|
||||||
|
data,
|
||||||
|
difficulty = 5,
|
||||||
|
signal = null,
|
||||||
|
progressCallback = null,
|
||||||
|
_threads = 1,
|
||||||
|
) {
|
||||||
console.debug("slow algo");
|
console.debug("slow algo");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let webWorkerURL = URL.createObjectURL(new Blob([
|
let webWorkerURL = URL.createObjectURL(new Blob([
|
||||||
@@ -8,15 +14,33 @@ export default function process(data, difficulty = 5, _threads = 1) {
|
|||||||
], { type: 'application/javascript' }));
|
], { type: 'application/javascript' }));
|
||||||
|
|
||||||
let worker = new Worker(webWorkerURL);
|
let worker = new Worker(webWorkerURL);
|
||||||
|
const terminate = () => {
|
||||||
|
worker.terminate();
|
||||||
|
if (signal != null) {
|
||||||
|
// clean up listener to avoid memory leak
|
||||||
|
signal.removeEventListener("abort", terminate);
|
||||||
|
if (signal.aborted) {
|
||||||
|
console.log("PoW aborted");
|
||||||
|
reject(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (signal != null) {
|
||||||
|
signal.addEventListener("abort", terminate, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
worker.onmessage = (event) => {
|
worker.onmessage = (event) => {
|
||||||
worker.terminate();
|
if (typeof event.data === "number") {
|
||||||
resolve(event.data);
|
progressCallback?.(event.data);
|
||||||
|
} else {
|
||||||
|
terminate();
|
||||||
|
resolve(event.data);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.onerror = (event) => {
|
worker.onerror = (event) => {
|
||||||
worker.terminate();
|
terminate();
|
||||||
reject();
|
reject(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
@@ -47,6 +71,9 @@ function processTask() {
|
|||||||
let hash;
|
let hash;
|
||||||
let nonce = 0;
|
let nonce = 0;
|
||||||
do {
|
do {
|
||||||
|
if (nonce & 1023 === 0) {
|
||||||
|
postMessage(nonce);
|
||||||
|
}
|
||||||
hash = await sha256(data + nonce++);
|
hash = await sha256(data + nonce++);
|
||||||
} while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0'));
|
} while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0'));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
export default function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) {
|
export default function process(
|
||||||
|
data,
|
||||||
|
difficulty = 5,
|
||||||
|
signal = null,
|
||||||
|
progressCallback = null,
|
||||||
|
threads = (navigator.hardwareConcurrency || 1),
|
||||||
|
) {
|
||||||
console.debug("fast algo");
|
console.debug("fast algo");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let webWorkerURL = URL.createObjectURL(new Blob([
|
let webWorkerURL = URL.createObjectURL(new Blob([
|
||||||
@@ -6,19 +12,36 @@ export default function process(data, difficulty = 5, threads = (navigator.hardw
|
|||||||
], { type: 'application/javascript' }));
|
], { type: 'application/javascript' }));
|
||||||
|
|
||||||
const workers = [];
|
const workers = [];
|
||||||
|
const terminate = () => {
|
||||||
|
workers.forEach((w) => w.terminate());
|
||||||
|
if (signal != null) {
|
||||||
|
// clean up listener to avoid memory leak
|
||||||
|
signal.removeEventListener("abort", terminate);
|
||||||
|
if (signal.aborted) {
|
||||||
|
console.log("PoW aborted");
|
||||||
|
reject(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (signal != null) {
|
||||||
|
signal.addEventListener("abort", terminate, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < threads; i++) {
|
for (let i = 0; i < threads; i++) {
|
||||||
let worker = new Worker(webWorkerURL);
|
let worker = new Worker(webWorkerURL);
|
||||||
|
|
||||||
worker.onmessage = (event) => {
|
worker.onmessage = (event) => {
|
||||||
workers.forEach(worker => worker.terminate());
|
if (typeof event.data === "number") {
|
||||||
worker.terminate();
|
progressCallback?.(event.data);
|
||||||
resolve(event.data);
|
} else {
|
||||||
|
terminate();
|
||||||
|
resolve(event.data);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.onerror = (event) => {
|
worker.onerror = (event) => {
|
||||||
worker.terminate();
|
terminate();
|
||||||
reject();
|
reject(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
@@ -55,6 +78,8 @@ function processTask() {
|
|||||||
let nonce = event.data.nonce;
|
let nonce = event.data.nonce;
|
||||||
let threads = event.data.threads;
|
let threads = event.data.threads;
|
||||||
|
|
||||||
|
const threadId = nonce;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const currentHash = await sha256(data + nonce);
|
const currentHash = await sha256(data + nonce);
|
||||||
const thisHash = new Uint8Array(currentHash);
|
const thisHash = new Uint8Array(currentHash);
|
||||||
@@ -78,7 +103,21 @@ function processTask() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldNonce = nonce;
|
||||||
nonce += threads;
|
nonce += threads;
|
||||||
|
|
||||||
|
// send a progress update every 1024 iterations. since each thread checks
|
||||||
|
// separate values, one simple way to do this is by bit masking the
|
||||||
|
// nonce for multiples of 1024. unfortunately, if the number of threads
|
||||||
|
// is not prime, only some of the threads will be sending the status
|
||||||
|
// update and they will get behind the others. this is slightly more
|
||||||
|
// complicated but ensures an even distribution between threads.
|
||||||
|
if (
|
||||||
|
nonce > oldNonce | 1023 && // we've wrapped past 1024
|
||||||
|
(nonce >> 10) % threads === threadId // and it's our turn
|
||||||
|
) {
|
||||||
|
postMessage(nonce);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postMessage({
|
postMessage({
|
||||||
|
|||||||
2
web/static/js/.gitignore
vendored
Normal file
2
web/static/js/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
(()=>{function p(r,n=5,t=navigator.hardwareConcurrency||1){return console.debug("fast algo"),new Promise((e,o)=>{let s=URL.createObjectURL(new Blob(["(",y(),")()"],{type:"application/javascript"})),a=[];for(let i=0;i<t;i++){let c=new Worker(s);c.onmessage=d=>{a.forEach(u=>u.terminate()),c.terminate(),e(d.data)},c.onerror=d=>{c.terminate(),o()},c.postMessage({data:r,difficulty:n,nonce:i,threads:t}),a.push(c)}URL.revokeObjectURL(s)})}function y(){return function(){let r=t=>{let e=new TextEncoder().encode(t);return crypto.subtle.digest("SHA-256",e.buffer)};function n(t){return Array.from(t).map(e=>e.toString(16).padStart(2,"0")).join("")}addEventListener("message",async t=>{let e=t.data.data,o=t.data.difficulty,s,a=t.data.nonce,i=t.data.threads;for(;;){let c=await r(e+a),d=new Uint8Array(c),u=!0;for(let m=0;m<o;m++){let l=Math.floor(m/2),g=m%2;if((d[l]>>(g===0?4:0)&15)!==0){u=!1;break}}if(u){s=n(d),console.log(s);break}a+=i}postMessage({hash:s,data:e,difficulty:o,nonce:a})})}.toString()}function f(r,n=5,t=1){return console.debug("slow algo"),new Promise((e,o)=>{let s=URL.createObjectURL(new Blob(["(",b(),")()"],{type:"application/javascript"})),a=new Worker(s);a.onmessage=i=>{a.terminate(),e(i.data)},a.onerror=i=>{a.terminate(),o()},a.postMessage({data:r,difficulty:n}),URL.revokeObjectURL(s)})}function b(){return function(){let r=n=>{let t=new TextEncoder().encode(n);return crypto.subtle.digest("SHA-256",t.buffer).then(e=>Array.from(new Uint8Array(e)).map(o=>o.toString(16).padStart(2,"0")).join(""))};addEventListener("message",async n=>{let t=n.data.data,e=n.data.difficulty,o,s=0;do o=await r(t+s++);while(o.substring(0,e)!==Array(e+1).join("0"));s-=1,postMessage({hash:o,data:t,difficulty:e,nonce:s})})}.toString()}var L={fast:p,slow:f},w=(r="",n={})=>{let t=new URL(r,window.location.href);return Object.entries(n).forEach(e=>{let[o,s]=e;t.searchParams.set(o,s)}),t.toString()},h=(r,n)=>w(`/.within.website/x/cmd/anubis/static/img/${r}.webp`,{cacheBuster:n});(async()=>{let r=document.getElementById("status"),n=document.getElementById("image"),t=document.getElementById("title"),e=document.getElementById("spinner"),o=JSON.parse(document.getElementById("anubis_version").textContent);r.innerHTML="Calculating...";let{challenge:s,rules:a}=await fetch("/.within.website/x/cmd/anubis/api/make-challenge",{method:"POST"}).then(l=>{if(!l.ok)throw new Error("Failed to fetch config");return l.json()}).catch(l=>{throw t.innerHTML="Oh no!",r.innerHTML=`Failed to fetch config: ${l.message}`,n.src=h("sad",o),e.innerHTML="",e.style.display="none",l}),i=L[a.algorithm];if(!i){t.innerHTML="Oh no!",r.innerHTML="Failed to resolve check algorithm. You may want to reload the page.",n.src=h("sad",o),e.innerHTML="",e.style.display="none";return}r.innerHTML=`Calculating...<br/>Difficulty: ${a.report_as}`;let c=Date.now(),{hash:d,nonce:u}=await i(s,a.difficulty),m=Date.now();console.log({hash:d,nonce:u}),t.innerHTML="Success!",r.innerHTML=`Done! Took ${m-c}ms, ${u} iterations`,n.src=h("happy",o),e.innerHTML="",e.style.display="none",setTimeout(()=>{let l=window.location.href;window.location.href=w("/.within.website/x/cmd/anubis/api/pass-challenge",{response:d,nonce:u,redir:l,elapsedTime:m-c})},250)})();})();
|
|
||||||
//# sourceMappingURL=main.mjs.map
|
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
2
xess/.gitignore
vendored
2
xess/.gitignore
vendored
@@ -1 +1 @@
|
|||||||
node_modules
|
xess.min.css
|
||||||
|
|||||||
6
xess/build.sh
Executable file
6
xess/build.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
postcss ./xess.css -o xess.min.css
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@xeserv/xess",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Xe's CSS",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"build": "postcss xess.css -o xess.min.css"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"cssnano": "^7.0.6",
|
|
||||||
"cssnano-preset-advanced": "^7.0.6",
|
|
||||||
"postcss-cli": "^11.0.0",
|
|
||||||
"postcss-import": "^16.1.0",
|
|
||||||
"postcss-import-url": "^7.2.0",
|
|
||||||
"postcss-url": "^10.1.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,12 +12,10 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go run github.com/a-h/templ/cmd/templ@latest generate
|
//go:generate go tool github.com/a-h/templ/cmd/templ generate
|
||||||
//go:generate npm ci
|
|
||||||
//go:generate npm run build
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed xess.min.css xess.css static
|
//go:embed *.css static
|
||||||
Static embed.FS
|
Static embed.FS
|
||||||
|
|
||||||
URL = "/.within.website/x/xess/xess.css"
|
URL = "/.within.website/x/xess/xess.css"
|
||||||
|
|||||||
1
xess/xess.min.css
vendored
1
xess/xess.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.850
|
// templ: version: v0.3.833
|
||||||
package xess
|
package xess
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
42
yeetfile.js
42
yeetfile.js
@@ -1,22 +1,28 @@
|
|||||||
go.install();
|
["amd64", "arm64", "riscv64"].forEach(goarch => {
|
||||||
|
[deb, rpm].forEach(method => method.build({
|
||||||
|
name: "anubis",
|
||||||
|
description: "Anubis weighs the souls of incoming HTTP requests and uses a sha256 proof-of-work challenge in order to protect upstream resources from scraper bots.",
|
||||||
|
homepage: "https://anubis.techaro.lol",
|
||||||
|
license: "MIT",
|
||||||
|
goarch,
|
||||||
|
|
||||||
["amd64", "arm64"].forEach(goarch => rpm.build({
|
documentation: {
|
||||||
name: "anubis",
|
"./README.md": "README.md",
|
||||||
description: "Anubis weighs the souls of incoming HTTP requests and uses a sha256 proof-of-work challenge in order to protect upstream resources from scraper bots.",
|
"./LICENSE": "LICENSE",
|
||||||
homepage: "https://xeiaso.net/blog/2025/anubis",
|
"./docs/docs/CHANGELOG.md": "CHANGELOG.md",
|
||||||
license: "MIT",
|
},
|
||||||
goarch,
|
|
||||||
|
|
||||||
build: (out) => {
|
build: (out) => {
|
||||||
// install Anubis binary
|
// install Anubis binary
|
||||||
go.build("-o", `${out}/usr/bin/anubis`);
|
go.build("-o", `${out}/usr/bin/anubis`, "./cmd/anubis");
|
||||||
|
|
||||||
// install systemd unit
|
// install systemd unit
|
||||||
yeet.run("mkdir", "-p", `${out}/usr/lib/systemd/system`);
|
yeet.run("mkdir", "-p", `${out}/usr/lib/systemd/system`);
|
||||||
yeet.run("cp", "run/anubis@.service", `${out}/usr/lib/systemd/system/anubis@.service`);
|
yeet.run("cp", "run/anubis@.service", `${out}/usr/lib/systemd/system/anubis@.service`);
|
||||||
|
|
||||||
// install default config
|
// install default config
|
||||||
yeet.run("mkdir", "-p", `${out}/etc/anubis`);
|
yeet.run("mkdir", "-p", `${out}/etc/anubis`);
|
||||||
yeet.run("cp", "run/anubis.env.default", `${out}/etc/anubis/anubis-default.env`);
|
yeet.run("cp", "run/anubis.env.default", `${out}/etc/anubis/anubis-default.env`);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user