Compare commits

...

7 Commits

Author SHA1 Message Date
Xe Iaso 953f85ec74 chore: spelling
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-03 06:24:51 -04:00
Xe Iaso 94ed2cb1b7 fix(run/anubis@.service): unique runtimedir per instance
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-02 22:10:20 -04: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
Victor Fernandes 292c470ada Set cookies to have the Secure flag default to true (#739)
* Set Cookies to use the Secure Flag and default SameSite to None

* Add secure flag test

* Updated changelog and documentation for secure flag option
2025-06-30 14:58:31 -04:00
Rafael Fontenelle 12453fdc00 Fix translations in pt-BR.json (#729)
Signed-off-by: Rafael Fontenelle <rffontenelle@users.noreply.github.com>
2025-06-30 14:14:24 -04:00
14 changed files with 83 additions and 38 deletions
+2 -1
View File
@@ -75,6 +75,7 @@ domainhere
dracula dracula
dronebl dronebl
droneblresponse droneblresponse
dropin
duckduckbot duckduckbot
eerror eerror
ellenjoe ellenjoe
@@ -237,6 +238,7 @@ risc
ruleset ruleset
runlevels runlevels
RUnlock RUnlock
runtimedir
sas sas
sasl sasl
Scumm Scumm
@@ -328,5 +330,4 @@ yoursite
Zenos Zenos
zizmor zizmor
zombocom zombocom
Zonbocom
zos zos
+4
View File
@@ -32,3 +32,7 @@ const APIPrefix = "/.within.website/x/cmd/anubis/api/"
// DefaultDifficulty is the default "difficulty" (number of leading zeroes) // DefaultDifficulty is the default "difficulty" (number of leading zeroes)
// that must be met by the client in order to pass the challenge. // that must be met by the client in order to pass the challenge.
const DefaultDifficulty = 4 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 = ""
+4
View File
@@ -50,7 +50,9 @@ var (
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") 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") 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") 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") 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") 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")
@@ -377,6 +379,7 @@ func main() {
anubis.CookieName = *cookiePrefix + "-auth" anubis.CookieName = *cookiePrefix + "-auth"
anubis.TestCookieName = *cookiePrefix + "-cookie-verification" anubis.TestCookieName = *cookiePrefix + "-cookie-verification"
anubis.ForcedLanguage = *forcedLanguage
// If OpenGraph configuration values are not set in the config file, use the // If OpenGraph configuration values are not set in the config file, use the
// values from flags / envvars. // values from flags / envvars.
@@ -403,6 +406,7 @@ func main() {
Target: *target, Target: *target,
WebmasterEmail: *webmasterEmail, WebmasterEmail: *webmasterEmail,
OpenGraph: policy.OpenGraph, OpenGraph: policy.OpenGraph,
CookieSecure: *cookieSecure,
}) })
if err != nil { if err != nil {
log.Fatalf("can't construct libanubis.Server: %v", err) log.Fatalf("can't construct libanubis.Server: %v", err)
+22
View File
@@ -11,11 +11,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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)). - 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 localization system. Find locale files in lib/localization/locales/.
- Implement a [development container](https://containers.dev/) manifest to make contributions easier. - 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)) - 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)) - 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 ## v1.20.0: Thancred Waters
+1
View File
@@ -67,6 +67,7 @@ Anubis uses these environment variables for configuration:
| `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_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 |
| `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. |
| `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. | | `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. |
| `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. | | `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. |
+5
View File
@@ -262,6 +262,7 @@ func TestCookieSettings(t *testing.T) {
CookieDomain: "127.0.0.1", CookieDomain: "127.0.0.1",
CookiePartitioned: true, CookiePartitioned: true,
CookieSecure: true,
CookieExpiration: anubis.CookieDefaultExpirationTime, CookieExpiration: anubis.CookieDefaultExpirationTime,
}) })
@@ -309,6 +310,10 @@ func TestCookieSettings(t *testing.T) {
if ckie.Partitioned != srv.opts.CookiePartitioned { if ckie.Partitioned != srv.opts.CookiePartitioned {
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned) 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)
}
} }
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) { func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898 // templ: version: v0.3.906
package metarefresh package metarefresh
//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.
+1
View File
@@ -44,6 +44,7 @@ type Options struct {
StripBasePrefix bool StripBasePrefix bool
OpenGraph config.OpenGraph OpenGraph config.OpenGraph
ServeRobotsTXT bool ServeRobotsTXT bool
CookieSecure bool
} }
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) { func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
+14 -6
View File
@@ -27,6 +27,7 @@ type CookieOpts struct {
Host string Host string
Path string Path string
Name string Name string
Expiry time.Duration
} }
func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) { func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
@@ -45,12 +46,17 @@ func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
} }
} }
if cookieOpts.Expiry == 0 {
cookieOpts.Expiry = s.opts.CookieExpiration
}
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: name, Name: name,
Value: cookieOpts.Value, Value: cookieOpts.Value,
Expires: time.Now().Add(s.opts.CookieExpiration), Expires: time.Now().Add(cookieOpts.Expiry),
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteNoneMode,
Domain: domain, Domain: domain,
Secure: s.opts.CookieSecure,
Partitioned: s.opts.CookiePartitioned, Partitioned: s.opts.CookiePartitioned,
Path: path, Path: path,
}) })
@@ -77,9 +83,10 @@ func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
Value: "", Value: "",
MaxAge: -1, MaxAge: -1,
Expires: time.Now().Add(-1 * time.Minute), Expires: time.Now().Add(-1 * time.Minute),
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteNoneMode,
Partitioned: s.opts.CookiePartitioned, Partitioned: s.opts.CookiePartitioned,
Domain: domain, Domain: domain,
Secure: s.opts.CookieSecure,
Path: path, Path: path,
}) })
} }
@@ -132,11 +139,12 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
} }
} }
http.SetCookie(w, &http.Cookie{ s.SetCookie(w, CookieOpts{
Name: anubis.TestCookieName,
Value: challengeStr, Value: challengeStr,
Expires: time.Now().Add(30 * time.Minute), Host: r.Host,
Path: "/", Path: "/",
Name: anubis.TestCookieName,
Expiry: 30 * time.Minute,
}) })
impl, ok := challenge.Get(rule.Challenge.Algorithm) impl, ok := challenge.Get(rule.Challenge.Algorithm)
+2 -2
View File
@@ -27,10 +27,10 @@
"static_check_endpoint": "Este é apenas um ponto de verificação para seu proxy reverso usar.", "static_check_endpoint": "Este é apenas um ponto de verificação para seu proxy reverso usar.",
"authorization_required": "Autorização necessária", "authorization_required": "Autorização necessária",
"cookies_disabled": "Seu navegador está configurado para desabilitar cookies. O Anubis requer cookies para o interesse legítimo de garantir que você seja um cliente válido. Habilite os cookies para este domínio.", "cookies_disabled": "Seu navegador está configurado para desabilitar cookies. O Anubis requer cookies para o interesse legítimo de garantir que você seja um cliente válido. Habilite os cookies para este domínio.",
"access_denied": "Acesso negado: código de errado", "access_denied": "Acesso negado: código de erro",
"dronebl_entry": "DroneBL relatou uma entrada", "dronebl_entry": "DroneBL relatou uma entrada",
"see_dronebl_lookup": "consulte", "see_dronebl_lookup": "consulte",
"internal_server_error": "Erro interno do servidor: o administrador configurou incorretamente o Anubis. Entre em contato com o administrador e peça para ele procurar os logs em torno dele.", "internal_server_error": "Erro interno do servidor: o administrador configurou incorretamente o Anubis. Entre em contato com o administrador e peça para analisar os logs relacionados.",
"invalid_redirect": "Redirecionamento inválido", "invalid_redirect": "Redirecionamento inválido",
"redirect_not_parseable": "URL de redirecionamento não analisável", "redirect_not_parseable": "URL de redirecionamento não analisável",
"redirect_domain_not_allowed": "Domínio de redirecionamento não permitido", "redirect_domain_not_allowed": "Domínio de redirecionamento não permitido",
+8 -2
View File
@@ -3,6 +3,7 @@ package localization
import ( import (
"embed" "embed"
"encoding/json" "encoding/json"
"github.com/TecharoHQ/anubis"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@@ -93,8 +94,13 @@ func (sl *SimpleLocalizer) T(messageID string) string {
return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID}) 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 { 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} return &SimpleLocalizer{Localizer: localizer}
} }
+1 -1
View File
@@ -12,7 +12,7 @@ CacheDirectory=anubis/%i
CacheDirectoryMode=0755 CacheDirectoryMode=0755
StateDirectory=anubis/%i StateDirectory=anubis/%i
StateDirectoryMode=0755 StateDirectoryMode=0755
RuntimeDirectory=anubis RuntimeDirectory=anubis/%i
RuntimeDirectoryMode=0755 RuntimeDirectoryMode=0755
ReadWritePaths=/run ReadWritePaths=/run
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898 // templ: version: v0.3.906
package web package web
//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.
-7
View File
@@ -212,11 +212,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
const t1 = Date.now(); const t1 = Date.now();
console.log({ hash, nonce }); 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) { if (userReadDetails) {
const container = document.getElementById("progress"); const container = document.getElementById("progress");
@@ -251,7 +246,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
container.onclick = onDetailsExpand; container.onclick = onDetailsExpand;
setTimeout(onDetailsExpand, 30000); setTimeout(onDetailsExpand, 30000);
} else { } else {
setTimeout(() => {
const redir = window.location.href; const redir = window.location.href;
window.location.replace( window.location.replace(
u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, { u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
@@ -261,7 +255,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
elapsedTime: t1 - t0, elapsedTime: t1 - t0,
}), }),
); );
}, 250);
} }
} catch (err) { } catch (err) {
ohNoes({ ohNoes({