mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-14 12:38:45 +00:00
Compare commits
8 Commits
Xe/decayma
...
Xe/content
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c6b7be42 | ||
|
|
88b3e457ee | ||
|
|
bb2b113b63 | ||
|
|
6c283d0cd9 | ||
|
|
0037e214a1 | ||
|
|
29ae2a4b87 | ||
|
|
401e18f29f | ||
|
|
63591866aa |
4
.github/actions/spelling/expect.txt
vendored
4
.github/actions/spelling/expect.txt
vendored
@@ -1,4 +1,7 @@
|
|||||||
acs
|
acs
|
||||||
|
Actorified
|
||||||
|
actorifiedstore
|
||||||
|
actorify
|
||||||
Aibrew
|
Aibrew
|
||||||
alibaba
|
alibaba
|
||||||
alrest
|
alrest
|
||||||
@@ -157,6 +160,7 @@ ifm
|
|||||||
Imagesift
|
Imagesift
|
||||||
imgproxy
|
imgproxy
|
||||||
impressum
|
impressum
|
||||||
|
inbox
|
||||||
inp
|
inp
|
||||||
internets
|
internets
|
||||||
IPTo
|
IPTo
|
||||||
|
|||||||
8
.github/workflows/ssh-ci.yml
vendored
8
.github/workflows/ssh-ci.yml
vendored
@@ -12,12 +12,14 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
ssh:
|
ssh:
|
||||||
if: github.repository == 'TecharoHQ/anubis'
|
if: github.repository == 'TecharoHQ/anubis'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: alrest-techarohq
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
host:
|
host:
|
||||||
- ubuntu@riscv64.techaro.lol
|
- riscv64
|
||||||
- ci@ppc64le.techaro.lol
|
- ppc64le
|
||||||
|
- aarch64-4k
|
||||||
|
- aarch64-16k
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ var (
|
|||||||
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
|
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")
|
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")
|
cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
|
||||||
|
cookieSameSite = flag.String("cookie-same-site", "None", "sets the same site option on Anubis cookies, will auto-downgrade None to Lax if cookie-secure is false. Valid values are None, Lax, Strict, and Default.")
|
||||||
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")
|
||||||
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
||||||
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
||||||
@@ -143,6 +144,22 @@ func parseBindNetFromAddr(address string) (string, string) {
|
|||||||
return "", address
|
return "", address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSameSite(s string) (http.SameSite) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "none":
|
||||||
|
return http.SameSiteNoneMode
|
||||||
|
case "lax":
|
||||||
|
return http.SameSiteLaxMode
|
||||||
|
case "strict":
|
||||||
|
return http.SameSiteStrictMode
|
||||||
|
case "default":
|
||||||
|
return http.SameSiteDefaultMode
|
||||||
|
default:
|
||||||
|
log.Fatalf("invalid cookie same-site mode: %s, valid values are None, Lax, Strict, and Default", s)
|
||||||
|
}
|
||||||
|
return http.SameSiteDefaultMode
|
||||||
|
}
|
||||||
|
|
||||||
func setupListener(network string, address string) (net.Listener, string) {
|
func setupListener(network string, address string) (net.Listener, string) {
|
||||||
formattedAddress := ""
|
formattedAddress := ""
|
||||||
|
|
||||||
@@ -432,6 +449,7 @@ func main() {
|
|||||||
WebmasterEmail: *webmasterEmail,
|
WebmasterEmail: *webmasterEmail,
|
||||||
OpenGraph: policy.OpenGraph,
|
OpenGraph: policy.OpenGraph,
|
||||||
CookieSecure: *cookieSecure,
|
CookieSecure: *cookieSecure,
|
||||||
|
CookieSameSite: parseSameSite(*cookieSameSite),
|
||||||
PublicUrl: *publicUrl,
|
PublicUrl: *publicUrl,
|
||||||
JWTRestrictionHeader: *jwtRestrictionHeader,
|
JWTRestrictionHeader: *jwtRestrictionHeader,
|
||||||
DifficultyInJWT: *difficultyInJWT,
|
DifficultyInJWT: *difficultyInJWT,
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
<!-- This changes the project to: -->
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103))
|
- Add `contentLength` variable to bot expressions.
|
||||||
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086))
|
- Add `COOKIE_SAME_SITE_MODE` to force anubis cookies SameSite value, and downgrade automatically from `None` to `Lax` if cookie is insecure.
|
||||||
- Add validation warning when persistent storage is used without setting signing keys
|
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
||||||
- Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925))
|
- Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
||||||
|
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086)).
|
||||||
|
- Add validation warning when persistent storage is used without setting signing keys.
|
||||||
|
- Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925)).
|
||||||
- Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend.
|
- Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend.
|
||||||
- Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines.
|
- Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines.
|
||||||
- Add the `DIFFICULTY_IN_JWT` option, which allows one to add the `difficulty` field in the JWT claims which indicates the difficulty of the token ([#1063](https://github.com/TecharoHQ/anubis/pull/1063))
|
- Add the `DIFFICULTY_IN_JWT` option, which allows one to add the `difficulty` field in the JWT claims which indicates the difficulty of the token ([#1063](https://github.com/TecharoHQ/anubis/pull/1063)).
|
||||||
- Ported the client-side JS to TypeScript to avoid egregious errors in the future.
|
- Ported the client-side JS to TypeScript to avoid egregious errors in the future.
|
||||||
- Fixes concurrency problems with very old browsers ([#1082](https://github.com/TecharoHQ/anubis/issues/1082)).
|
- Fixes concurrency problems with very old browsers ([#1082](https://github.com/TecharoHQ/anubis/issues/1082)).
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,34 @@ Your directory tree should look like this, assuming your data is in `./your_fold
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
### Header-based overlay dispatch
|
||||||
|
|
||||||
|
If you run BotStopper in a multi-tenant environment where each tenant needs its own branding, BotStopper supports the ability to use request header values to direct asset reads to different folders under your `OVERLAY_FOLDER`. One of the most common ways to do this is based on the HTTP Host of the request. For example, if you set `ASSET_LOOKUP_HEADER=Host` in BotStopper's environment:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$OVERLAY_FOLDER
|
||||||
|
├── static
|
||||||
|
│ ├── css
|
||||||
|
│ │ ├── custom.css
|
||||||
|
│ │ └── eyesore.css
|
||||||
|
│ └── img
|
||||||
|
│ ├── happy.webp
|
||||||
|
│ ├── pensive.webp
|
||||||
|
│ └── reject.webp
|
||||||
|
└── test.anubis.techaro.lol
|
||||||
|
└── static
|
||||||
|
├── css
|
||||||
|
│ └── custom.css
|
||||||
|
└── img
|
||||||
|
├── happy.webp
|
||||||
|
├── pensive.webp
|
||||||
|
└── reject.webp
|
||||||
|
```
|
||||||
|
|
||||||
|
Requests to `test.anubis.techaro.lol` will load assets in `$OVERLAY_FOLDER/test.anubis.techaro.lol/static` and all other requests will load them from `$OVERLAY_FOLDER/static`.
|
||||||
|
|
||||||
|
For an example, look at [the testdata folder in the BotStopper repo](https://github.com/TecharoHQ/botstopper/tree/main/testdata).
|
||||||
|
|
||||||
### Custom CSS
|
### 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.
|
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.
|
||||||
@@ -199,7 +227,9 @@ $ du -hs *
|
|||||||
|
|
||||||
## Custom HTML templates
|
## Custom HTML templates
|
||||||
|
|
||||||
If you need to completely control the HTML layout of all Anubis pages, you can customize the entire page with `USE_TEMPLATES=true`. This uses Go's standard library [html/template](https://pkg.go.dev/html/template) package to template HTML responses. In order to use this, you must define the following templates:
|
If you need to completely control the HTML layout of all Anubis pages, you can customize the entire page with `USE_TEMPLATES=true`. This uses Go's standard library [html/template](https://pkg.go.dev/html/template) package to template HTML responses. Your templates can contain whatever HTML you want. The only catch is that you MUST include `{{ .Head }}` in the `<head>` element for challenge pages, and you MUST include `{{ .Body }}` in the `<body>` element for all pages.
|
||||||
|
|
||||||
|
In order to use this, you must define the following templates:
|
||||||
|
|
||||||
| Template path | Usage |
|
| Template path | Usage |
|
||||||
| :----------------------------------------- | :---------------------------------------------- |
|
| :----------------------------------------- | :---------------------------------------------- |
|
||||||
@@ -207,6 +237,12 @@ If you need to completely control the HTML layout of all Anubis pages, you can c
|
|||||||
| `$OVERLAY_FOLDER/templates/error.tmpl` | Error pages |
|
| `$OVERLAY_FOLDER/templates/error.tmpl` | Error pages |
|
||||||
| `$OVERLAY_FOLDER/templates/impressum.tmpl` | [Impressum](./configuration/impressum.mdx) page |
|
| `$OVERLAY_FOLDER/templates/impressum.tmpl` | [Impressum](./configuration/impressum.mdx) page |
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
Currently HTML templates don't work together with [Header-based overlay dispatch](#header-based-overlay-dispatch). This is a known issue that will be fixed soon. If you enable header-based overlay dispatch, BotStopper will use the global `templates` folder instead of using the templates present in the overlay.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
Here are minimal (but working) examples for each template:
|
Here are minimal (but working) examples for each template:
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Anubis supports multiple challenge methods:
|
Anubis supports multiple challenge methods:
|
||||||
|
|
||||||
- [Meta Refresh](./metarefresh.mdx)
|
- [Meta Refresh](./metarefresh.mdx)
|
||||||
|
- [Preact](./preact.mdx)
|
||||||
- [Proof of Work](./proof-of-work.mdx)
|
- [Proof of Work](./proof-of-work.mdx)
|
||||||
|
|
||||||
Read the documentation to know which method is best for you.
|
Read the documentation to know which method is best for you.
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ Anubis exposes the following variables to expressions:
|
|||||||
| :-------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
|
| :-------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
|
||||||
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
|
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
|
||||||
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
|
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
|
||||||
|
| `contentLength` | `int64` | The numerical value of the `Content-Length` header. |
|
||||||
| `load_1m` | `double` | The current system load average over the last one minute. This is useful for making [load-based checks](#using-the-system-load-average). |
|
| `load_1m` | `double` | The current system load average over the last one minute. This is useful for making [load-based checks](#using-the-system-load-average). |
|
||||||
| `load_5m` | `double` | The current system load average over the last five minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
|
| `load_5m` | `double` | The current system load average over the last five minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
|
||||||
| `load_15m` | `double` | The current system load average over the last fifteen minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
|
| `load_15m` | `double` | The current system load average over the last fifteen minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
|
||||||
|
|||||||
@@ -2,8 +2,16 @@
|
|||||||
title: Setting up Anubis
|
title: Setting up Anubis
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import EnterpriseOnly from "@site/src/components/EnterpriseOnly";
|
||||||
import RandomKey from "@site/src/components/RandomKey";
|
import RandomKey from "@site/src/components/RandomKey";
|
||||||
|
|
||||||
|
export const EO = () => (
|
||||||
|
<>
|
||||||
|
<EnterpriseOnly link="./botstopper/" />
|
||||||
|
<div style={{ marginBottom: "0.5rem" }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
Anubis is meant to sit between your reverse proxy (such as Nginx or Caddy) and your target service. One instance of Anubis must be used per service you are protecting.
|
Anubis is meant to sit between your reverse proxy (such as Nginx or Caddy) and your target service. One instance of Anubis must be used per service you are protecting.
|
||||||
|
|
||||||
<center>
|
<center>
|
||||||
@@ -59,26 +67,31 @@ Currently the following settings are configurable via the policy file:
|
|||||||
Anubis uses these environment variables for configuration:
|
Anubis uses these environment variables for configuration:
|
||||||
|
|
||||||
| Environment Variable | Default value | Explanation |
|
| Environment Variable | Default value | Explanation |
|
||||||
| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| :----------------------------- | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `ASSET_LOOKUP_HEADER` | unset | <EO /> If set, use the contents of this header in requests when looking up custom assets in `OVERLAY_FOLDER`. See [Header-based overlay dispatch](./botstopper.mdx#header-based-overlay-dispatch) for more details. |
|
||||||
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
|
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
|
||||||
| `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. |
|
||||||
|
| `CHALLENGE_TITLE` | unset | <EO /> If set, override the translation stack to show a custom title for challenge pages such as "Making sure your connection is secure!". See [Customizing messages](./botstopper.mdx#customizing-messages) for more details. |
|
||||||
| `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 this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.<br/><br/>Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. |
|
| `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 this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.<br/><br/>Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. |
|
||||||
| `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. |
|
| `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. |
|
||||||
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
||||||
| `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. |
|
||||||
| `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. |
|
| `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. |
|
||||||
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
||||||
|
| `COOKIE_SAME_SITE` | `None` | Controls the cookie’s [`SameSite` attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value). Allowed: `None`, `Lax`, `Strict`, `Default`. `None` permits cross-site use but modern browsers require it to be **Secure**—so if `COOKIE_SECURE=false` or you serve over plain HTTP, use `Lax` (recommended) or `Strict` or the cookie will be rejected. `Default` uses the Go runtime’s `SameSiteDefaultMode`. `None` will be downgraded to `Lax` automatically if cookie is set NOT to be secure. |
|
||||||
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||||
| `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. |
|
| `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. |
|
||||||
| `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. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
|
| `ED25519_PRIVATE_KEY_HEX` | 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. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
|
||||||
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
|
| `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. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
|
||||||
|
| `ERROR_TITLE` | unset | <EO /> If set, override the translation stack to show a custom title for error pages such as "Something went wrong!". See [Customizing messages](./botstopper.mdx#customizing-messages) for more details. |
|
||||||
| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
|
| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
|
||||||
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
||||||
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
||||||
| `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
| `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
||||||
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
||||||
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
||||||
|
| `OVERLAY_FOLDER` | unset | <EO /> If set, treat the given path as an [overlay folder](./botstopper.mdx#custom-images-and-css), allowing you to customize CSS, fonts, images, and add other assets to BotStopper deployments. |
|
||||||
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
|
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
|
||||||
| `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). |
|
| `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). |
|
||||||
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.<br/><br/>Note that if you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here. |
|
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.<br/><br/>Note that if you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here. |
|
||||||
@@ -89,6 +102,7 @@ Anubis uses these environment variables for configuration:
|
|||||||
| `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`. |
|
| `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. |
|
| `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. |
|
||||||
| `USE_SIMPLIFIED_EXPLANATION` | false | If set to `true`, replaces the text when clicking "Why am I seeing this?" with a more simplified text for a non-tech-savvy audience. |
|
| `USE_SIMPLIFIED_EXPLANATION` | false | If set to `true`, replaces the text when clicking "Why am I seeing this?" with a more simplified text for a non-tech-savvy audience. |
|
||||||
|
| `USE_TEMPLATES` | false | <EO /> If set to `true`, enable [custom HTML template support](./botstopper.mdx#custom-html-templates), allowing you to completely rewrite how BotStopper renders its HTML pages. |
|
||||||
| `WEBMASTER_EMAIL` | unset | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators. |
|
| `WEBMASTER_EMAIL` | unset | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators. |
|
||||||
| `XFF_STRIP_PRIVATE` | `true` | If set, strip private addresses from `X-Forwarded-For` headers. To unset this, you must set `XFF_STRIP_PRIVATE=false` or `--xff-strip-private=false`. |
|
| `XFF_STRIP_PRIVATE` | `true` | If set, strip private addresses from `X-Forwarded-For` headers. To unset this, you must set `XFF_STRIP_PRIVATE=false` or `--xff-strip-private=false`. |
|
||||||
|
|
||||||
|
|||||||
11
docs/src/components/EnterpriseOnly/index.jsx
Normal file
11
docs/src/components/EnterpriseOnly/index.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
export default function EnterpriseOnly({ link }) {
|
||||||
|
return (
|
||||||
|
<a className={styles.link} href={link}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<span className={styles.label}>BotStopper Only</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
docs/src/components/EnterpriseOnly/styles.module.css
Normal file
18
docs/src/components/EnterpriseOnly/styles.module.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: #16a34a; /* green-500 */
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.5rem 1rem; /* py-2 px-4 */
|
||||||
|
border-radius: 9999px; /* rounded-full */
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg approximation */
|
||||||
|
display: inline-flex; /* flex */
|
||||||
|
align-items: center; /* items-center */
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
107
internal/actorify/actorify.go
Normal file
107
internal/actorify/actorify.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// Package actorify lets you transform a parallel operation into a serialized
|
||||||
|
// operation via the Actor pattern[1].
|
||||||
|
//
|
||||||
|
// [1]: https://en.wikipedia.org/wiki/Actor_model
|
||||||
|
package actorify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func z[Z any]() Z {
|
||||||
|
var z Z
|
||||||
|
return z
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrActorDied is returned when the actor inbox or reply channel was closed.
|
||||||
|
ErrActorDied = errors.New("actorify: the actor inbox or reply channel was closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler is a function alias for the underlying logic the Actor should call.
|
||||||
|
type Handler[Input, Output any] func(ctx context.Context, input Input) (Output, error)
|
||||||
|
|
||||||
|
// Actor is a serializing wrapper that runs a function in a background goroutine.
|
||||||
|
// Whenever the Call method is invoked, a message is sent to the actor's inbox and then
|
||||||
|
// the callee waits for a response. Depending on how busy the actor is, this may take
|
||||||
|
// a moment.
|
||||||
|
type Actor[Input, Output any] struct {
|
||||||
|
handler Handler[Input, Output]
|
||||||
|
inbox chan *message[Input, Output]
|
||||||
|
}
|
||||||
|
|
||||||
|
type message[Input, Output any] struct {
|
||||||
|
ctx context.Context
|
||||||
|
arg Input
|
||||||
|
reply chan reply[Output]
|
||||||
|
}
|
||||||
|
|
||||||
|
type reply[Output any] struct {
|
||||||
|
output Output
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// New constructs a new Actor and starts its background thread. Cancel the context and you cancel
|
||||||
|
// the Actor.
|
||||||
|
func New[Input, Output any](ctx context.Context, handler Handler[Input, Output]) *Actor[Input, Output] {
|
||||||
|
result := &Actor[Input, Output]{
|
||||||
|
handler: handler,
|
||||||
|
inbox: make(chan *message[Input, Output], 32),
|
||||||
|
}
|
||||||
|
|
||||||
|
go result.handle(ctx)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Actor[Input, Output]) handle(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
close(a.inbox)
|
||||||
|
return
|
||||||
|
case msg, ok := <-a.inbox:
|
||||||
|
if !ok {
|
||||||
|
if msg.reply != nil {
|
||||||
|
close(msg.reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := a.handler(msg.ctx, msg.arg)
|
||||||
|
|
||||||
|
reply := reply[Output]{
|
||||||
|
output: result,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.reply <- reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call calls the Actor with a given Input and returns the handler's Output.
|
||||||
|
//
|
||||||
|
// This only works with unary functions by design. If you need to have more inputs, define
|
||||||
|
// a struct type to use as a container.
|
||||||
|
func (a *Actor[Input, Output]) Call(ctx context.Context, input Input) (Output, error) {
|
||||||
|
replyCh := make(chan reply[Output])
|
||||||
|
|
||||||
|
a.inbox <- &message[Input, Output]{
|
||||||
|
arg: input,
|
||||||
|
reply: replyCh,
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case reply, ok := <-replyCh:
|
||||||
|
if !ok {
|
||||||
|
return z[Output](), ErrActorDied
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.output, reply.err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return z[Output](), context.Cause(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -299,6 +299,7 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
CookieDomain: "127.0.0.1",
|
CookieDomain: "127.0.0.1",
|
||||||
CookiePartitioned: true,
|
CookiePartitioned: true,
|
||||||
CookieSecure: true,
|
CookieSecure: true,
|
||||||
|
CookieSameSite: http.SameSiteNoneMode,
|
||||||
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -339,6 +340,65 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
if ckie.Secure != srv.opts.CookieSecure {
|
if ckie.Secure != srv.opts.CookieSecure {
|
||||||
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
||||||
}
|
}
|
||||||
|
if ckie.SameSite != srv.opts.CookieSameSite {
|
||||||
|
t.Errorf("wanted same site option %v, got: %v", srv.opts.CookieSameSite, ckie.SameSite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieSettingsSameSiteNoneModeDowngradedToLaxWhenUnsecure(t *testing.T) {
|
||||||
|
pol := loadPolicies(t, "testdata/zero_difficulty.yaml", 0)
|
||||||
|
|
||||||
|
srv := spawnAnubis(t, Options{
|
||||||
|
Next: http.NewServeMux(),
|
||||||
|
Policy: pol,
|
||||||
|
|
||||||
|
CookieDomain: "127.0.0.1",
|
||||||
|
CookiePartitioned: true,
|
||||||
|
CookieSecure: false,
|
||||||
|
CookieSameSite: http.SameSiteNoneMode,
|
||||||
|
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cli := httpClient(t)
|
||||||
|
chall := makeChallenge(t, ts, cli)
|
||||||
|
|
||||||
|
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusFound {
|
||||||
|
resp.Write(os.Stderr)
|
||||||
|
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ckie *http.Cookie
|
||||||
|
for _, cookie := range resp.Cookies() {
|
||||||
|
t.Logf("%#v", cookie)
|
||||||
|
if cookie.Name == anubis.CookieName {
|
||||||
|
ckie = cookie
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ckie == nil {
|
||||||
|
t.Errorf("Cookie %q not found", anubis.CookieName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Domain != "127.0.0.1" {
|
||||||
|
t.Errorf("cookie domain is wrong, wanted 127.0.0.1, got: %s", ckie.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Partitioned != srv.opts.CookiePartitioned {
|
||||||
|
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Secure != srv.opts.CookieSecure {
|
||||||
|
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
||||||
|
}
|
||||||
|
if ckie.SameSite != http.SameSiteLaxMode {
|
||||||
|
t.Errorf("wanted same site Lax option %v, got: %v", http.SameSiteLaxMode, ckie.SameSite)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Options struct {
|
|||||||
OpenGraph config.OpenGraph
|
OpenGraph config.OpenGraph
|
||||||
ServeRobotsTXT bool
|
ServeRobotsTXT bool
|
||||||
CookieSecure bool
|
CookieSecure bool
|
||||||
|
CookieSameSite http.SameSite
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
PublicUrl string
|
PublicUrl string
|
||||||
JWTRestrictionHeader string
|
JWTRestrictionHeader string
|
||||||
|
|||||||
15
lib/http.go
15
lib/http.go
@@ -56,6 +56,8 @@ func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
|||||||
var domain = s.opts.CookieDomain
|
var domain = s.opts.CookieDomain
|
||||||
var name = anubis.CookieName
|
var name = anubis.CookieName
|
||||||
var path = "/"
|
var path = "/"
|
||||||
|
var sameSite = s.opts.CookieSameSite
|
||||||
|
|
||||||
if cookieOpts.Name != "" {
|
if cookieOpts.Name != "" {
|
||||||
name = cookieOpts.Name
|
name = cookieOpts.Name
|
||||||
}
|
}
|
||||||
@@ -72,11 +74,15 @@ func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
|||||||
cookieOpts.Expiry = s.opts.CookieExpiration
|
cookieOpts.Expiry = s.opts.CookieExpiration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.opts.CookieSameSite == http.SameSiteNoneMode && !s.opts.CookieSecure {
|
||||||
|
sameSite = http.SameSiteLaxMode
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: cookieOpts.Value,
|
Value: cookieOpts.Value,
|
||||||
Expires: time.Now().Add(cookieOpts.Expiry),
|
Expires: time.Now().Add(cookieOpts.Expiry),
|
||||||
SameSite: http.SameSiteNoneMode,
|
SameSite: sameSite,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Secure: s.opts.CookieSecure,
|
Secure: s.opts.CookieSecure,
|
||||||
Partitioned: s.opts.CookiePartitioned,
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
@@ -88,6 +94,8 @@ func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
|||||||
var domain = s.opts.CookieDomain
|
var domain = s.opts.CookieDomain
|
||||||
var name = anubis.CookieName
|
var name = anubis.CookieName
|
||||||
var path = "/"
|
var path = "/"
|
||||||
|
var sameSite = s.opts.CookieSameSite
|
||||||
|
|
||||||
if cookieOpts.Name != "" {
|
if cookieOpts.Name != "" {
|
||||||
name = cookieOpts.Name
|
name = cookieOpts.Name
|
||||||
}
|
}
|
||||||
@@ -99,13 +107,16 @@ func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
|||||||
domain = etld
|
domain = etld
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.opts.CookieSameSite == http.SameSiteNoneMode && !s.opts.CookieSecure {
|
||||||
|
sameSite = http.SameSiteLaxMode
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: "",
|
Value: "",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Expires: time.Now().Add(-1 * time.Minute),
|
Expires: time.Now().Add(-1 * time.Minute),
|
||||||
SameSite: http.SameSiteNoneMode,
|
SameSite: sameSite,
|
||||||
Partitioned: s.opts.CookiePartitioned,
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Secure: s.opts.CookieSecure,
|
Secure: s.opts.CookieSecure,
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) {
|
|||||||
switch name {
|
switch name {
|
||||||
case "remoteAddress":
|
case "remoteAddress":
|
||||||
return cr.Header.Get("X-Real-Ip"), true
|
return cr.Header.Get("X-Real-Ip"), true
|
||||||
|
case "contentLength":
|
||||||
|
return cr.ContentLength, true
|
||||||
case "host":
|
case "host":
|
||||||
return cr.Host, true
|
return cr.Host, true
|
||||||
case "method":
|
case "method":
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func BotEnvironment() (*cel.Env, error) {
|
|||||||
return New(
|
return New(
|
||||||
// Variables exposed to CEL programs:
|
// Variables exposed to CEL programs:
|
||||||
cel.Variable("remoteAddress", cel.StringType),
|
cel.Variable("remoteAddress", cel.StringType),
|
||||||
|
cel.Variable("contentLength", cel.IntType),
|
||||||
cel.Variable("host", cel.StringType),
|
cel.Variable("host", cel.StringType),
|
||||||
cel.Variable("method", cel.StringType),
|
cel.Variable("method", cel.StringType),
|
||||||
cel.Variable("userAgent", cel.StringType),
|
cel.Variable("userAgent", cel.StringType),
|
||||||
|
|||||||
82
lib/store/actorifiedstore.go
Normal file
82
lib/store/actorifiedstore.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal/actorify"
|
||||||
|
)
|
||||||
|
|
||||||
|
type unit struct{}
|
||||||
|
|
||||||
|
type ActorifiedStore struct {
|
||||||
|
Interface
|
||||||
|
|
||||||
|
deleteActor *actorify.Actor[string, unit]
|
||||||
|
getActor *actorify.Actor[string, []byte]
|
||||||
|
setActor *actorify.Actor[*actorSetReq, unit]
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type actorSetReq struct {
|
||||||
|
key string
|
||||||
|
value []byte
|
||||||
|
expiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActorifiedStore(backend Interface) *ActorifiedStore {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
result := &ActorifiedStore{
|
||||||
|
Interface: backend,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
result.deleteActor = actorify.New(ctx, result.actorDelete)
|
||||||
|
result.getActor = actorify.New(ctx, backend.Get)
|
||||||
|
result.setActor = actorify.New(ctx, result.actorSet)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ActorifiedStore) Close() { a.cancel() }
|
||||||
|
|
||||||
|
func (a *ActorifiedStore) Delete(ctx context.Context, key string) error {
|
||||||
|
if _, err := a.deleteActor.Call(ctx, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ActorifiedStore) Get(ctx context.Context, key string) ([]byte, error) {
|
||||||
|
return a.getActor.Call(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ActorifiedStore) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
|
||||||
|
if _, err := a.setActor.Call(ctx, &actorSetReq{
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
expiry: expiry,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ActorifiedStore) actorDelete(ctx context.Context, key string) (unit, error) {
|
||||||
|
if err := a.Interface.Delete(ctx, key); err != nil {
|
||||||
|
return unit{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return unit{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ActorifiedStore) actorSet(ctx context.Context, req *actorSetReq) (unit, error) {
|
||||||
|
if err := a.Interface.Set(ctx, req.key, req.value, req.expiry); err != nil {
|
||||||
|
return unit{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return unit{}, nil
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ func (Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface
|
|||||||
|
|
||||||
go result.cleanupThread(ctx)
|
go result.cleanupThread(ctx)
|
||||||
|
|
||||||
return result, nil
|
return store.NewActorifiedStore(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid parses and validates the bbolt store Config or returns
|
// Valid parses and validates the bbolt store Config or returns
|
||||||
|
|||||||
@@ -7,25 +7,32 @@ if [ "$#" -ne 1 ]; then
|
|||||||
echo "Usage: rigging.sh <user@host>"
|
echo "Usage: rigging.sh <user@host>"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
declare -A Hosts
|
||||||
|
|
||||||
|
Hosts["riscv64"]="ubuntu@riscv64.techaro.lol" # GOARCH=riscv64 GOOS=linux
|
||||||
|
Hosts["ppc64le"]="ci@ppc64le.techaro.lol" # GOARCH=ppc64le GOOS=linux
|
||||||
|
Hosts["aarch64-4k"]="rocky@192.168.2.52" # GOARCH=arm64 GOOS=linux 4k page size
|
||||||
|
Hosts["aarch64-16k"]="ci@192.168.2.28" # GOARCH=arm64 GOOS=linux 16k page size
|
||||||
|
|
||||||
CIRunnerImage="ghcr.io/techarohq/anubis/ci-runner:latest"
|
CIRunnerImage="ghcr.io/techarohq/anubis/ci-runner:latest"
|
||||||
RunID=${GITHUB_RUN_ID:-$(uuidgen)}
|
RunID=${GITHUB_RUN_ID:-$(uuidgen)}
|
||||||
RunFolder="anubis/runs/${RunID}"
|
RunFolder="anubis/runs/${RunID}"
|
||||||
Target="${1}"
|
Target="${Hosts["$1"]}"
|
||||||
|
|
||||||
ssh "${Target}" uname -av
|
ssh "${Target}" uname -av >/dev/null
|
||||||
ssh "${Target}" mkdir -p "${RunFolder}"
|
ssh "${Target}" mkdir -p "${RunFolder}"
|
||||||
git archive HEAD | ssh "${Target}" tar xC "${RunFolder}"
|
git archive HEAD | ssh "${Target}" tar xC "${RunFolder}"
|
||||||
|
|
||||||
ssh "${Target}" <<EOF
|
ssh "${Target}" <<EOF
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
set -x
|
set -x
|
||||||
mkdir -p "anubis/cache/{go,go-build,node}"
|
mkdir -p anubis/cache/{go,go-build,node}
|
||||||
podman pull ${CIRunnerImage}
|
podman pull ${CIRunnerImage}
|
||||||
podman run --rm -it \
|
podman run --rm -it \
|
||||||
-v "\$HOME/${RunFolder}:/app/anubis" \
|
-v "\$HOME/${RunFolder}:/app/anubis:z" \
|
||||||
-v "\$HOME/anubis/cache/go:/root/go" \
|
-v "\$HOME/anubis/cache/go:/root/go:z" \
|
||||||
-v "\$HOME/anubis/cache/go-build:/root/.cache/go-build" \
|
-v "\$HOME/anubis/cache/go-build:/root/.cache/go-build:z" \
|
||||||
-v "\$HOME/anubis/cache/node:/root/.npm" \
|
-v "\$HOME/anubis/cache/node:/root/.npm:z" \
|
||||||
-w /app/anubis \
|
-w /app/anubis \
|
||||||
${CIRunnerImage} \
|
${CIRunnerImage} \
|
||||||
sh /app/anubis/test/ssh-ci/in-container.sh
|
sh /app/anubis/test/ssh-ci/in-container.sh
|
||||||
|
|||||||
Reference in New Issue
Block a user