Compare commits

...

8 Commits

Author SHA1 Message Date
Xe Iaso
8babcf87ca docs(botstopper): remove otherwise
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-03 23:05:04 +00:00
Xe Iaso
322224446a docs(botstopper): fix broken links
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-03 23:04:21 +00:00
Xe Iaso
3cf9275f21 Update metadata
check-spelling run (push) for Xe/docs-botstopper

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>
2025-07-03 22:58:56 +00:00
Xe Iaso
4e176b557c docs: add BotStopper docs from the git repo
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-03 22:57:11 +00:00
Xe Iaso
888b7d6e77 fix(run/anubis@.service): unique runtimedir per instance (#750)
* fix(run/anubis@.service): unique runtimedir per instance

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

* chore: spelling

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-03 10:29:05 +00:00
Martin
0e43138324 feat(localization): Add option for forcing a language (#742)
* Add forcesLanguage option

* Change comments for forced language option

* Add changes to CHANGELOG.md

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-07-02 05:33:00 +00:00
Xe Iaso
c981c23f7e chore: npm run generate
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-02 05:25:10 +00:00
Xe Iaso
9f0c5e974e fix(web/main): remove the success interstitial (#745)
I'm gonna be totally honest here, I'm still not sure why #564 is still
an issue. This is really confusing and I'm going to totally throw out
how Anubis issues challenges and redo it with Valkey (#201, #622).

The problem seems to be that I assume that the makeChallenge function in
package lib is idempotent for the same client. I have no idea why this
would be inconsistent, but for some reason it is and I'm just at a loss
for words as to why this is happening.

This stops the bleeding by improving the UX as a stopgap.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-01 23:44:38 +00:00
14 changed files with 272 additions and 25 deletions

View File

@@ -20,11 +20,13 @@ bdba
berr
bingbot
bitcoin
bitrate
blogging
Bluesky
blueskybot
boi
botnet
botstopper
BPort
Brightbot
broked
@@ -52,6 +54,7 @@ ckie
cloudflare
Codespaces
confd
connnection
containerbuild
coreutils
Cotoyogi
@@ -75,6 +78,7 @@ domainhere
dracula
dronebl
droneblresponse
dropin
duckduckbot
eerror
ellenjoe
@@ -92,6 +96,7 @@ facebookgo
Factset
fastcgi
fediverse
ffprobe
finfos
Firecrawl
flagenv
@@ -198,6 +203,7 @@ omgilibot
openai
opengraph
openrc
oswald
pag
palemoon
Pangu
@@ -237,6 +243,7 @@ risc
ruleset
runlevels
RUnlock
runtimedir
sas
sasl
Scumm
@@ -270,6 +277,8 @@ SVCNAME
tagline
tarballs
tarrif
tbn
tbr
techaro
techarohq
templ
@@ -328,5 +337,4 @@ yoursite
Zenos
zizmor
zombocom
Zonbocom
zos

View File

@@ -32,3 +32,7 @@ const APIPrefix = "/.within.website/x/cmd/anubis/api/"
// DefaultDifficulty is the default "difficulty" (number of leading zeroes)
// that must be met by the client in order to pass the challenge.
const DefaultDifficulty = 4
// ForcedLanguage is the language being used instead of the one of the request's Accept-Language header
// if being set.
var ForcedLanguage = ""

View File

@@ -50,6 +50,7 @@ var (
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
cookiePrefix = flag.String("cookie-prefix", "techaro.lol-anubis", "prefix for browser cookies created by Anubis")
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
@@ -378,6 +379,7 @@ func main() {
anubis.CookieName = *cookiePrefix + "-auth"
anubis.TestCookieName = *cookiePrefix + "-cookie-verification"
anubis.ForcedLanguage = *forcedLanguage
// If OpenGraph configuration values are not set in the config file, use the
// values from flags / envvars.

View File

@@ -10,15 +10,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
<!-- This changes the project to: -->
- Add `COOKIE_SECURE` option to set the cookie [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies)
- Sets cookie defaults to use [SameSite: None](https://web.dev/articles/samesite-cookies-explained)
- Determine the `BIND_NETWORK`/`--bind-network` value from the bind address ([#677](https://github.com/TecharoHQ/anubis/issues/677)).
- Implement localization system. Find locale files in lib/localization/locales/.
- Implement a [development container](https://containers.dev/) manifest to make contributions easier.
- Fix dynamic cookie domains functionality ([#731](https://github.com/TecharoHQ/anubis/pull/731))
- Add option for custom cookie prefix ([#732](https://github.com/TecharoHQ/anubis/pull/732))
- Remove the "Success" interstitial after a proof of work challenge is concluded.
- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
### Potentially breaking changes
The following potentially breaking change applies to native installs with systemd only:
Each instance of systemd service template now has a unique `RuntimeDirectory`, as opposed to each instance of the service sharing a `RuntimeDirectory`. This change was made to avoid [the `RuntimeDirectory` getting nuked any time one of the Anubis instances restarts](https://github.com/TecharoHQ/anubis/issues/748).
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/sock` and additionally configure your HTTP load balancer as appropriate.
If you need the legacy behaviour, install this [systemd unit dropin](https://www.flatcar.org/docs/latest/setup/systemd/drop-in-units/):
```systemd
# /etc/systemd/system/anubis@.service.d/50-runtimedir.conf
[Service]
RuntimeDirectory=anubis
```
## v1.20.0: Thancred Waters

View File

@@ -0,0 +1,215 @@
---
title: "Commercial support and an unbranded version"
---
If you want to use Anubis but organizational policies prevent you from using the branding that the open source project ships, we offer a commercial version of Anubis named BotStopper. BotStopper builds off of the open source core of Anubis and offers organizations more control over the branding, including but not limited to:
- Custom images for different states of the challenge process (in process, success, failure)
- Custom CSS and fonts
- Custom titles for the challenge and error pages
- "Anubis" replaced with "BotStopper" across the UI
- A private bug tracker for issues
In the near future this will expand to:
- A private challenge implementation that does advanced fingerprinting to check if the client is a genuine browser or not
- Advanced fingerprinting via [Thoth-based advanced checks](./thoth.mdx)
In order to sign up for BotStopper, please do one of the following:
- Sign up [on GitHub Sponsors](https://github.com/sponsors/Xe) at the $50 per month tier or higher
- Email [sales@techaro.lol](mailto:sales@techaro.lol) with your requirements for invoicing, please note that custom invoicing will cost more than using GitHub Sponsors for understandable overhead reasons
## Installation
Install BotStopper like you would Anubis, but replace the image reference. EG:
```diff
-ghcr.io/techarohq/anubis:latest
+ghcr.io/techarohq/botstopper/anubis:latest
```
### Binary packages
Binary packages are available [in the GitHub Releases page](https://github.com/TecharoHQ/botstopper/releases), the main difference is that the package name is `techaro-botstopper`, the systemd service is `techaro-botstopper@your-instance.service`, the binary is `/usr/bin/botstopper`, and the configuration is in `/etc/techaro-botstopper`. All other instructions in the [native package install guide](./native-install.mdx) apply.
### Docker / Podman
In order to pull the BotStopper image, you need to [authenticate with GitHub's Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry).
```text
docker login ghcr.io -u your-username --password-stdin
```
Then you can use the image as normal.
### Kubernetes
If you are using Kubernetes, you will need to create an image pull secret:
```text
kubectl create secret docker-registry \
techarohq-botstopper \
--docker-server ghcr.io \
--docker-username your-username \
--docker-password your-access-token \
--docker-email your@email.address
```
Then attach it to your Deployment:
```diff
spec:
securityContext:
fsGroup: 1000
+ imagePullSecrets:
+ - name: techarohq-botstopper
```
## Configuration
### Docker compose
Follow [the upstream Docker compose directions](https://anubis.techaro.lol/docs/admin/environments/docker-compose) with the following additional options:
```diff
anubis:
image: ghcr.io/techarohq/botstopper/anubis:latest
environment:
BIND: ":8080"
DIFFICULTY: "4"
METRICS_BIND: ":9090"
SERVE_ROBOTS_TXT: "true"
TARGET: "http://nginx"
OG_PASSTHROUGH: "true"
OG_EXPIRY_TIME: "24h"
+ # botstopper config here
+ CHALLENGE_TITLE: "Doing math for your connnection!"
+ ERROR_TITLE: "Something went wrong!"
+ OVERLAY_FOLDER: /assets
+ volumes:
+ - "./your_folder:/assets"
```
#### Example
There is an example in [docker-compose.yaml](https://github.com/TecharoHQ/botstopper/blob/main/docker-compose.yaml). Start the example with `docker compose up`:
```text
docker compose up -d
```
And then open [https://botstopper.local.cetacean.club:8443](https://botstopper.local.cetacean.club:8443) in your browser.
> [!NOTE]
> This uses locally signed sacrificial TLS certificates stored in `./demo/pki`. Your browser will rightly reject these. Here is what the example looks like:
>
> ![](/img/botstopper/example-screenshot.webp)
## Custom images and CSS
Anubis uses an internal filesystem that contains CSS, JavaScript, and images. The BotStopper variant of Anubis lets you specify an overlay folder with the environment variable `OVERLAY_FOLDER`. The contents of this folder will be overlaid on top of Anubis' internal filesystem, allowing you to easily customize the images and CSS.
Your directory tree should look like this, assuming your data is in `./your_folder`:
```text
./your_folder
└── static
├── css
│ └── custom.css
└── img
├── happy.webp
├── pensive.webp
└── reject.webp
```
For an example directory tree using some off-the-shelf images the Tango icon set, see the [testdata](https://github.com/TecharoHQ/botstopper/tree/main/testdata/static/img) folder.
### Custom CSS
CSS customization is done mainly with CSS variables. View [the example custom CSS file](https://github.com/TecharoHQ/botstopper/blob/main/testdata/static/css/custom.css) for more information about what can be customized.
### Custom fonts
If you want to add custom fonts, copy the `woff2` files alongside your `custom.css` file and then include them with the [`@font-face` CSS at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face):
```css
@font-face {
font-family: "Oswald";
font-style: normal;
font-weight: 200 900;
font-display: swap;
src: url("./fonts/oswald.woff2") format("woff2");
}
```
Then adjust your CSS variables accordingly:
```css
:root {
--body-sans-font: Oswald, sans-serif;
--body-preformatted-font: monospace;
--body-title-font: serif;
}
```
To convert `.ttf` fonts to [Web-optimized woff2 fonts](https://www.w3.org/TR/WOFF2/), use the `woff2_compress` command from the `woff2` or `woff2-tools` package:
```console
$ woff2_compress oswald.ttf
Processing oswald.ttf => oswald.woff2
Compressed 159517 to 70469.
```
Then you can import and use it as normal.
### Customizing images
Anubis uses three images to visually communicate the state of the program. These are:
| Image name | Intended message | Example |
| :------------- | :----------------------------------------------- | :-------------------------------- |
| `happy.webp` | You have passed validation, all is good | ![](/img/botstopper/happy.webp) |
| `pensive.webp` | Checking is running, hold steady until it's done | ![](/img/botstopper/pensive.webp) |
| `reject.webp` | Something went wrong, this is a terminal state | ![](/img/botstopper/reject.webp) |
To make your own images at the optimal quality, use the following ffmpeg command:
```text
ffmpeg -i /path/to/image -vf scale=-1:384 happy.webp
```
`ffprobe` should report something like this on the generated images:
```text
Input #0, webp_pipe, from 'happy.webp':
Duration: N/A, bitrate: N/A
Stream #0:0: Video: webp, none, 25 fps, 25 tbr, 25 tbn
```
In testing 384 by 384 pixels gives the best balance between filesize, quality, and clarity.
```text
$ du -hs *
4.0K happy.webp
12K pensive.webp
8.0K reject.webp
```
## Customizing messages
You can customize messages using the following environment variables:
| Message | Environment variable | Default |
| :------------------- | :------------------- | :----------------------------------------- |
| Challenge page title | `CHALLENGE_TITLE` | `Ensuring the security of your connection` |
| Error page title | `ERROR_TITLE` | `Error` |
For example:
```sh
# /etc/techaro-botstopper/gitea.env
CHALLENGE_TITLE="Wait a moment please!"
ERROR_TITLE="Client error"
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/static/img/botstopper/happy.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
docs/static/img/botstopper/pensive.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
docs/static/img/botstopper/reject.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.906
package metarefresh
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@@ -3,6 +3,7 @@ package localization
import (
"embed"
"encoding/json"
"github.com/TecharoHQ/anubis"
"net/http"
"strings"
"sync"
@@ -57,14 +58,14 @@ func NewLocalizationService() *LocalizationService {
globalService = &LocalizationService{bundle: bundle}
})
// Safety check - if globalService is still nil, create a minimal one
if globalService == nil {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
globalService = &LocalizationService{bundle: bundle}
}
return globalService
}
@@ -93,8 +94,13 @@ func (sl *SimpleLocalizer) T(messageID string) string {
return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
}
// GetLocalizer creates a localizer based on the request's Accept-Language header
// GetLocalizer creates a localizer based on the request's Accept-Language header or forcedLanguage option
func GetLocalizer(r *http.Request) *SimpleLocalizer {
localizer := NewLocalizationService().GetLocalizerFromRequest(r)
var localizer *i18n.Localizer
if anubis.ForcedLanguage == "" {
localizer = NewLocalizationService().GetLocalizerFromRequest(r)
} else {
localizer = NewLocalizationService().GetLocalizer(anubis.ForcedLanguage)
}
return &SimpleLocalizer{Localizer: localizer}
}

View File

@@ -12,7 +12,7 @@ CacheDirectory=anubis/%i
CacheDirectoryMode=0755
StateDirectory=anubis/%i
StateDirectoryMode=0755
RuntimeDirectory=anubis
RuntimeDirectory=anubis/%i
RuntimeDirectoryMode=0755
ReadWritePaths=/run

2
web/index_templ.go generated
View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
// templ: version: v0.3.906
package web
//lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@@ -212,11 +212,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
const t1 = Date.now();
console.log({ hash, nonce });
title.innerHTML = t('success');
status.innerHTML = `${t('done_took')} ${t1 - t0}ms, ${nonce} ${t('iterations')}`;
image.src = imageURL("happy", anubisVersion, basePrefix);
progress.style.display = "none";
if (userReadDetails) {
const container = document.getElementById("progress");
@@ -251,17 +246,15 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
container.onclick = onDetailsExpand;
setTimeout(onDetailsExpand, 30000);
} else {
setTimeout(() => {
const redir = window.location.href;
window.location.replace(
u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
response: hash,
nonce,
redir,
elapsedTime: t1 - t0,
}),
);
}, 250);
const redir = window.location.href;
window.location.replace(
u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
response: hash,
nonce,
redir,
elapsedTime: t1 - t0,
}),
);
}
} catch (err) {
ohNoes({