Compare commits

...

4 Commits

Author SHA1 Message Date
Xe Iaso f844fffd1e v1.15.1: Zenos yae Galvus: Echo 1 (#181)
* version 1.15.0 (#144)

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

* cmd/anubis actually check the result with the correct difficulty

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

* v1.15.1: Zenos yae Galvus: Echo 1

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Henri Vasserman <henv@hot.ee>
2025-03-31 18:27:01 -04:00
Xe Iaso 4b8efcba9b version 1.15.0
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-03-27 15:31:13 -04:00
Xe Iaso 7d4be0dcec Apply bits of the cookie settings PR one by one (#140)
Enables uses to change the cookie domain and partitioned flags.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-03-27 15:24:03 -04:00
Hans5958 d1d63d9c18 docs: fix broken link to default policy file (#137) 2025-03-27 08:43:37 -04:00
11 changed files with 203 additions and 67 deletions
+6 -1
View File
@@ -64,8 +64,13 @@ jobs:
~/.cache/ms-playwright ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
- name: install playwright browsers
run: |
npx --yes playwright@1.50.1 install --with-deps
npx --yes playwright@1.50.1 run-server --port 3000 &
- name: Build - name: Build
run: go build ./... run: go build ./...
- name: Test - name: Test
run: go test ./... run: go test -v ./...
+1 -1
View File
@@ -1 +1 @@
1.14.2 1.15.1
+8 -4
View File
@@ -34,6 +34,8 @@ 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")
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") metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to") metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to")
@@ -189,10 +191,12 @@ func main() {
} }
s, err := libanubis.New(libanubis.Options{ s, err := libanubis.New(libanubis.Options{
Next: rp, Next: rp,
Policy: policy, Policy: policy,
ServeRobotsTXT: *robotsTxt, ServeRobotsTXT: *robotsTxt,
PrivateKey: priv, PrivateKey: priv,
CookieDomain: *cookieDomain,
CookiePartitioned: *cookiePartitioned,
}) })
if err != nil { if err != nil {
log.Fatalf("can't construct libanubis.Server: %v", err) log.Fatalf("can't construct libanubis.Server: %v", err)
+28
View File
@@ -11,7 +11,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## 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,6 +44,9 @@ 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
- 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
+2
View File
@@ -45,6 +45,8 @@ Anubis uses these environment variables for configuration:
| :------------------------ | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | :------------------------ | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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_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` | | 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. | | `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
+1 -1
View File
@@ -52,7 +52,7 @@ Here is a minimal policy file that will protect against most scraper bots:
} }
``` ```
This allows requests to [`/.well-known`](https://en.wikipedia.org/wiki/Well-known_URI), `/favicon.ico`, `/robots.txt`, and challenges any request that has the word `Mozilla` in its User-Agent string. The [default policy file](https://github.com/TecharoHQ/anubis/blob/main/cmd/anubis/botPolicies.json) is a bit more cohesive, but this should be more than enough for most users. This allows requests to [`/.well-known`](https://en.wikipedia.org/wiki/Well-known_URI), `/favicon.ico`, `/robots.txt`, and challenges any request that has the word `Mozilla` in its User-Agent string. The [default policy file](https://github.com/TecharoHQ/anubis/blob/main/data/botPolicies.json) is a bit more cohesive, but this should be more than enough for most users.
If no rules match the request, it is allowed through. If no rules match the request, it is allowed through.
+22 -18
View File
@@ -166,10 +166,6 @@ func startPlaywright(t *testing.T) {
} }
func TestPlaywrightBrowser(t *testing.T) { func TestPlaywrightBrowser(t *testing.T) {
if os.Getenv("CI") == "true" {
t.Skip("XXX(Xe): This is broken in CI, will fix later")
}
if os.Getenv("DONT_USE_NETWORK") != "" { if os.Getenv("DONT_USE_NETWORK") != "" {
t.Skip("test requires network egress") t.Skip("test requires network egress")
return return
@@ -225,12 +221,20 @@ func TestPlaywrightBrowser(t *testing.T) {
t.Skip("skipping hard challenge with deadline") t.Skip("skipping hard challenge with deadline")
} }
perfomedAction := executeTestCase(t, tc, typ, anubisURL) var perfomedAction action
var err error
for i := 0; i < 5; i++ {
perfomedAction, err = executeTestCase(t, tc, typ, anubisURL)
if perfomedAction == tc.action {
break
}
time.Sleep(time.Duration(i+1) * 250 * time.Millisecond)
}
if perfomedAction != tc.action { if perfomedAction != 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, perfomedAction)
} else { }
t.Logf("test passed") if err != nil {
t.Fatalf("test error: %v", err)
} }
}) })
} }
@@ -247,14 +251,14 @@ func buildBrowserConnect(name string) string {
return u.String() return u.String()
} }
func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anubisURL string) action { func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anubisURL string) (action, error) {
deadline, _ := t.Deadline() deadline, _ := t.Deadline()
browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{ browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{
ExposeNetwork: playwright.String("<loopback>"), ExposeNetwork: playwright.String("<loopback>"),
}) })
if err != nil { if err != nil {
t.Fatalf("could not connect to remote browser: %v", err) return "", fmt.Errorf("could not connect to remote browser: %w", err)
} }
defer browser.Close() defer browser.Close()
@@ -266,13 +270,13 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
UserAgent: playwright.String(tc.userAgent), UserAgent: playwright.String(tc.userAgent),
}) })
if err != nil { if err != nil {
t.Fatalf("could not create context: %v", err) return "", fmt.Errorf("could not create context: %w", err)
} }
defer ctx.Close() defer ctx.Close()
page, err := ctx.NewPage() page, err := ctx.NewPage()
if err != nil { if err != nil {
t.Fatalf("could not create page: %v", err) return "", fmt.Errorf("could not create page: %w", err)
} }
defer page.Close() defer page.Close()
@@ -283,7 +287,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
Timeout: pwTimeout(tc, deadline), Timeout: pwTimeout(tc, deadline),
}) })
if err != nil { if err != nil {
pwFail(t, page, "could not navigate to test server: %v", err) return "", pwFail(t, page, "could not navigate to test server: %v", err)
} }
hadChallenge := false hadChallenge := false
@@ -294,7 +298,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
hadChallenge = true hadChallenge = true
case actionDeny: case actionDeny:
checkImage(t, tc, deadline, page, "#image[src*=sad]") checkImage(t, tc, deadline, page, "#image[src*=sad]")
return actionDeny return actionDeny, nil
} }
// Ensure protected resource was provided. // Ensure protected resource was provided.
@@ -317,9 +321,9 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
} }
if hadChallenge { if hadChallenge {
return actionChallenge return actionChallenge, nil
} else { } else {
return actionAllow return actionAllow, nil
} }
} }
@@ -342,11 +346,11 @@ func checkImage(t *testing.T, tc testCase, deadline time.Time, page playwright.P
} }
} }
func pwFail(t *testing.T, page playwright.Page, format string, args ...any) { func pwFail(t *testing.T, page playwright.Page, format string, args ...any) error {
t.Helper() t.Helper()
saveScreenshot(t, page) saveScreenshot(t, page)
t.Fatalf(format, args...) return fmt.Errorf(format, args...)
} }
func pwTimeout(tc testCase, deadline time.Time) *float64 { func pwTimeout(tc testCase, deadline time.Time) *float64 {
+3
View File
@@ -0,0 +1,3 @@
*.png
*.txt
*.html
+37 -30
View File
@@ -67,6 +67,10 @@ type Options struct {
Policy *policy.ParsedConfig Policy *policy.ParsedConfig
ServeRobotsTXT bool ServeRobotsTXT bool
PrivateKey ed25519.PrivateKey PrivateKey ed25519.PrivateKey
CookieDomain string
CookieName string
CookiePartitioned bool
} }
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) { func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
@@ -108,6 +112,7 @@ func New(opts Options) (*Server, error) {
priv: opts.PrivateKey, priv: opts.PrivateKey,
pub: opts.PrivateKey.Public().(ed25519.PublicKey), pub: opts.PrivateKey.Public().(ed25519.PublicKey),
policy: opts.Policy, policy: opts.Policy,
opts: opts,
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
} }
@@ -140,13 +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
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] opts Options
ChallengeDifficulty int DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
} }
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -217,7 +222,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
s.next.ServeHTTP(w, r) s.next.ServeHTTP(w, r)
return return
case config.RuleDeny: case config.RuleDeny:
ClearCookie(w) s.ClearCookie(w)
lg.Info("explicit deny") lg.Info("explicit deny")
if rule == nil { if rule == nil {
lg.Error("rule is nil, cannot calculate checksum") lg.Error("rule is nil, cannot calculate checksum")
@@ -236,7 +241,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
case config.RuleChallenge: case config.RuleChallenge:
lg.Debug("challenge requested") lg.Debug("challenge requested")
default: default:
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)
return return
} }
@@ -244,21 +249,21 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
ckie, err := r.Cookie(anubis.CookieName) ckie, err := r.Cookie(anubis.CookieName)
if err != nil { if err != nil {
lg.Debug("cookie not found", "path", r.URL.Path) lg.Debug("cookie not found", "path", r.URL.Path)
ClearCookie(w) s.ClearCookie(w)
s.RenderIndex(w, r) s.RenderIndex(w, r)
return return
} }
if err := ckie.Valid(); err != nil { if err := ckie.Valid(); err != nil {
lg.Debug("cookie is invalid", "err", err) lg.Debug("cookie is invalid", "err", err)
ClearCookie(w) s.ClearCookie(w)
s.RenderIndex(w, r) s.RenderIndex(w, r)
return return
} }
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() { if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
lg.Debug("cookie expired", "path", r.URL.Path) lg.Debug("cookie expired", "path", r.URL.Path)
ClearCookie(w) s.ClearCookie(w)
s.RenderIndex(w, r) s.RenderIndex(w, r)
return return
} }
@@ -269,7 +274,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
if err != nil || !token.Valid { if err != nil || !token.Valid {
lg.Debug("invalid token", "path", r.URL.Path, "err", err) lg.Debug("invalid token", "path", r.URL.Path, "err", err)
ClearCookie(w) s.ClearCookie(w)
s.RenderIndex(w, r) s.RenderIndex(w, r)
return return
} }
@@ -284,7 +289,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
claims, ok := token.Claims.(jwt.MapClaims) claims, ok := token.Claims.(jwt.MapClaims)
if !ok { if !ok {
lg.Debug("invalid token claims type", "path", r.URL.Path) lg.Debug("invalid token claims type", "path", r.URL.Path)
ClearCookie(w) s.ClearCookie(w)
s.RenderIndex(w, r) s.RenderIndex(w, r)
return return
} }
@@ -292,7 +297,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
if claims["challenge"] != challenge { if claims["challenge"] != challenge {
lg.Debug("invalid challenge", "path", r.URL.Path) lg.Debug("invalid challenge", "path", r.URL.Path)
ClearCookie(w) s.ClearCookie(w)
s.RenderIndex(w, r) s.RenderIndex(w, r)
return return
} }
@@ -309,7 +314,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
if subtle.ConstantTimeCompare([]byte(claims["response"].(string)), []byte(calculated)) != 1 { if subtle.ConstantTimeCompare([]byte(claims["response"].(string)), []byte(calculated)) != 1 {
lg.Debug("invalid response", "path", r.URL.Path) lg.Debug("invalid response", "path", r.URL.Path)
failedValidations.Inc() failedValidations.Inc()
ClearCookie(w) s.ClearCookie(w)
s.RenderIndex(w, r) s.RenderIndex(w, r)
return return
} }
@@ -372,7 +377,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
nonceStr := r.FormValue("nonce") nonceStr := r.FormValue("nonce")
if nonceStr == "" { if nonceStr == "" {
ClearCookie(w) s.ClearCookie(w)
lg.Debug("no nonce") lg.Debug("no nonce")
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return return
@@ -380,7 +385,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
elapsedTimeStr := r.FormValue("elapsedTime") elapsedTimeStr := r.FormValue("elapsedTime")
if elapsedTimeStr == "" { if elapsedTimeStr == "" {
ClearCookie(w) s.ClearCookie(w)
lg.Debug("no elapsedTime") lg.Debug("no elapsedTime")
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return return
@@ -388,7 +393,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64) elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
if err != nil { if err != nil {
ClearCookie(w) s.ClearCookie(w)
lg.Debug("elapsedTime doesn't parse", "err", err) lg.Debug("elapsedTime doesn't parse", "err", err)
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return return
@@ -404,7 +409,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
nonce, err := strconv.Atoi(nonceStr) nonce, err := strconv.Atoi(nonceStr)
if err != nil { if err != nil {
ClearCookie(w) s.ClearCookie(w)
lg.Debug("nonce doesn't parse", "err", err) lg.Debug("nonce doesn't parse", "err", err)
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return return
@@ -414,7 +419,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
calculated := internal.SHA256sum(calcString) calculated := internal.SHA256sum(calcString)
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 { if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
ClearCookie(w) s.ClearCookie(w)
lg.Debug("hash does not match", "got", response, "want", calculated) lg.Debug("hash does not match", "got", response, "want", calculated)
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()
@@ -422,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)) {
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
@@ -442,17 +447,19 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
tokenString, err := token.SignedString(s.priv) tokenString, err := token.SignedString(s.priv)
if err != nil { if err != nil {
lg.Error("failed to sign JWT", "err", err) lg.Error("failed to sign JWT", "err", err)
ClearCookie(w) s.ClearCookie(w)
templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return return
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: anubis.CookieName, Name: anubis.CookieName,
Value: tokenString, Value: tokenString,
Expires: time.Now().Add(24 * 7 * time.Hour), Expires: time.Now().Add(24 * 7 * time.Hour),
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
Path: "/", Domain: s.opts.CookieDomain,
Partitioned: s.opts.CookiePartitioned,
Path: "/",
}) })
challengesValidated.Inc() challengesValidated.Inc()
+93 -11
View File
@@ -1,15 +1,18 @@
package lib package lib
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/lib/policy"
) )
func spawnAnubis(t *testing.T, h http.Handler) string { func loadPolicies(t *testing.T, fname string) *policy.ParsedConfig {
t.Helper() t.Helper()
policy, err := LoadPoliciesOrDefault("", anubis.DefaultDifficulty) policy, err := LoadPoliciesOrDefault("", anubis.DefaultDifficulty)
@@ -17,23 +20,102 @@ func spawnAnubis(t *testing.T, h http.Handler) string {
t.Fatal(err) t.Fatal(err)
} }
s, err := New(Options{ return policy
Next: h, }
Policy: policy,
ServeRobotsTXT: true, func spawnAnubis(t *testing.T, opts Options) *Server {
}) t.Helper()
s, err := New(opts)
if err != nil { if err != nil {
t.Fatalf("can't construct libanubis.Server: %v", err) t.Fatalf("can't construct libanubis.Server: %v", err)
} }
ts := httptest.NewServer(s) return s
t.Log(ts.URL) }
t.Cleanup(func() { func TestCookieSettings(t *testing.T) {
ts.Close() pol := loadPolicies(t, "")
pol.DefaultDifficulty = 0
srv := spawnAnubis(t, Options{
Next: http.NewServeMux(),
Policy: pol,
CookieDomain: "local.cetacean.club",
CookiePartitioned: true,
CookieName: t.Name(),
}) })
return ts.URL ts := httptest.NewServer(internal.DefaultXRealIP("127.0.0.1", srv))
defer ts.Close()
cli := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := cli.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 = struct {
Challenge string `json:"challenge"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
t.Fatalf("can't read challenge response body: %v", err)
}
nonce := 0
elapsedTime := 420
redir := "/"
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
calculated := internal.SHA256sum(calcString)
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.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.Domain != "local.cetacean.club" {
t.Errorf("cookie domain is wrong, wanted local.cetacean.club, 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 == nil {
t.Errorf("Cookie %q not found", anubis.CookieName)
}
} }
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) { func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
+2 -1
View File
@@ -7,13 +7,14 @@ import (
"github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis"
) )
func ClearCookie(w http.ResponseWriter) { func (s *Server) ClearCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: anubis.CookieName, Name: anubis.CookieName,
Value: "", Value: "",
Expires: time.Now().Add(-1 * time.Hour), Expires: time.Now().Add(-1 * time.Hour),
MaxAge: -1, MaxAge: -1,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
Domain: s.opts.CookieDomain,
}) })
} }