Compare commits
5 Commits
revert-134
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35e0a8179a | ||
|
|
f844fffd1e | ||
|
|
4b8efcba9b | ||
|
|
7d4be0dcec | ||
|
|
d1d63d9c18 |
7
.github/workflows/go.yml
vendored
@@ -64,8 +64,13 @@ jobs:
|
||||
~/.cache/ms-playwright
|
||||
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
|
||||
run: go build ./...
|
||||
|
||||
- name: Test
|
||||
run: go test ./...
|
||||
run: go test -v ./...
|
||||
|
||||
@@ -34,6 +34,8 @@ var (
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
@@ -189,10 +191,12 @@ func main() {
|
||||
}
|
||||
|
||||
s, err := libanubis.New(libanubis.Options{
|
||||
Next: rp,
|
||||
Policy: policy,
|
||||
ServeRobotsTXT: *robotsTxt,
|
||||
PrivateKey: priv,
|
||||
Next: rp,
|
||||
Policy: policy,
|
||||
ServeRobotsTXT: *robotsTxt,
|
||||
PrivateKey: priv,
|
||||
CookieDomain: *cookieDomain,
|
||||
CookiePartitioned: *cookiePartitioned,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
|
||||
@@ -11,7 +11,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## v1.15.2
|
||||
|
||||
Zenos yae Galvus: Echo 2
|
||||
|
||||
The mascot has been updated with art by [CELPHASE](https://bsky.app/profile/celphase.bsky.social).
|
||||
|
||||
## 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
|
||||
- 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
|
||||
- Introduced integration tests using Playwright
|
||||
- Refactor & Split up Anubis into cmd and lib.go
|
||||
@@ -19,6 +50,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
|
||||
- Linting fixes
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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_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. |
|
||||
| `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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ const config: Config = {
|
||||
title: 'Anubis',
|
||||
logo: {
|
||||
alt: 'A happy jackal woman with brown hair and red eyes',
|
||||
src: 'img/happy.webp',
|
||||
src: 'img/favicon.webp',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
|
||||
BIN
docs/static/img/android-chrome-512x512.png
vendored
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 106 KiB |
BIN
docs/static/img/favicon.ico
vendored
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/favicon.webp
vendored
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
docs/static/img/happy.webp
vendored
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 30 KiB |
@@ -166,10 +166,6 @@ func startPlaywright(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") != "" {
|
||||
t.Skip("test requires network egress")
|
||||
return
|
||||
@@ -225,12 +221,20 @@ func TestPlaywrightBrowser(t *testing.T) {
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{
|
||||
ExposeNetwork: playwright.String("<loopback>"),
|
||||
})
|
||||
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()
|
||||
|
||||
@@ -266,13 +270,13 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||
UserAgent: playwright.String(tc.userAgent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("could not create context: %v", err)
|
||||
return "", fmt.Errorf("could not create context: %w", err)
|
||||
}
|
||||
defer ctx.Close()
|
||||
|
||||
page, err := ctx.NewPage()
|
||||
if err != nil {
|
||||
t.Fatalf("could not create page: %v", err)
|
||||
return "", fmt.Errorf("could not create page: %w", err)
|
||||
}
|
||||
defer page.Close()
|
||||
|
||||
@@ -283,7 +287,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||
Timeout: pwTimeout(tc, deadline),
|
||||
})
|
||||
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
|
||||
@@ -294,7 +298,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||
hadChallenge = true
|
||||
case actionDeny:
|
||||
checkImage(t, tc, deadline, page, "#image[src*=sad]")
|
||||
return actionDeny
|
||||
return actionDeny, nil
|
||||
}
|
||||
|
||||
// Ensure protected resource was provided.
|
||||
@@ -317,9 +321,9 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||
}
|
||||
|
||||
if hadChallenge {
|
||||
return actionChallenge
|
||||
return actionChallenge, nil
|
||||
} 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()
|
||||
|
||||
saveScreenshot(t, page)
|
||||
t.Fatalf(format, args...)
|
||||
return fmt.Errorf(format, args...)
|
||||
}
|
||||
|
||||
func pwTimeout(tc testCase, deadline time.Time) *float64 {
|
||||
|
||||
3
internal/test/var/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.png
|
||||
*.txt
|
||||
*.html
|
||||
@@ -67,6 +67,10 @@ type Options struct {
|
||||
Policy *policy.ParsedConfig
|
||||
ServeRobotsTXT bool
|
||||
PrivateKey ed25519.PrivateKey
|
||||
|
||||
CookieDomain string
|
||||
CookieName string
|
||||
CookiePartitioned bool
|
||||
}
|
||||
|
||||
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||
@@ -108,6 +112,7 @@ func New(opts Options) (*Server, error) {
|
||||
priv: opts.PrivateKey,
|
||||
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
||||
policy: opts.Policy,
|
||||
opts: opts,
|
||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||
}
|
||||
|
||||
@@ -140,13 +145,13 @@ func New(opts Options) (*Server, error) {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
next http.Handler
|
||||
priv ed25519.PrivateKey
|
||||
pub ed25519.PublicKey
|
||||
policy *policy.ParsedConfig
|
||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||
ChallengeDifficulty int
|
||||
mux *http.ServeMux
|
||||
next http.Handler
|
||||
priv ed25519.PrivateKey
|
||||
pub ed25519.PublicKey
|
||||
policy *policy.ParsedConfig
|
||||
opts Options
|
||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
case config.RuleDeny:
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
lg.Info("explicit deny")
|
||||
if rule == nil {
|
||||
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:
|
||||
lg.Debug("challenge requested")
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -244,21 +249,21 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
ckie, err := r.Cookie(anubis.CookieName)
|
||||
if err != nil {
|
||||
lg.Debug("cookie not found", "path", r.URL.Path)
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
s.RenderIndex(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ckie.Valid(); err != nil {
|
||||
lg.Debug("cookie is invalid", "err", err)
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
s.RenderIndex(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
|
||||
lg.Debug("cookie expired", "path", r.URL.Path)
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
s.RenderIndex(w, r)
|
||||
return
|
||||
}
|
||||
@@ -269,7 +274,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
s.RenderIndex(w, r)
|
||||
return
|
||||
}
|
||||
@@ -284,7 +289,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
lg.Debug("invalid token claims type", "path", r.URL.Path)
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
s.RenderIndex(w, r)
|
||||
return
|
||||
}
|
||||
@@ -292,7 +297,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if claims["challenge"] != challenge {
|
||||
lg.Debug("invalid challenge", "path", r.URL.Path)
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
s.RenderIndex(w, r)
|
||||
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 {
|
||||
lg.Debug("invalid response", "path", r.URL.Path)
|
||||
failedValidations.Inc()
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
s.RenderIndex(w, r)
|
||||
return
|
||||
}
|
||||
@@ -372,7 +377,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
nonceStr := r.FormValue("nonce")
|
||||
if nonceStr == "" {
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("no nonce")
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
@@ -380,7 +385,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
elapsedTimeStr := r.FormValue("elapsedTime")
|
||||
if elapsedTimeStr == "" {
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("no elapsedTime")
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
@@ -388,7 +393,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
||||
if err != nil {
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
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)
|
||||
return
|
||||
@@ -404,7 +409,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
nonce, err := strconv.Atoi(nonceStr)
|
||||
if err != nil {
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
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)
|
||||
return
|
||||
@@ -414,7 +419,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
calculated := internal.SHA256sum(calcString)
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
||||
ClearCookie(w)
|
||||
s.ClearCookie(w)
|
||||
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)
|
||||
failedValidations.Inc()
|
||||
@@ -422,9 +427,9 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// compare the leading zeroes
|
||||
if !strings.HasPrefix(response, strings.Repeat("0", s.ChallengeDifficulty)) {
|
||||
ClearCookie(w)
|
||||
lg.Debug("difficulty check failed", "response", response, "difficulty", s.ChallengeDifficulty)
|
||||
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
||||
s.ClearCookie(w)
|
||||
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)
|
||||
failedValidations.Inc()
|
||||
return
|
||||
@@ -442,17 +447,19 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
tokenString, err := token.SignedString(s.priv)
|
||||
if err != nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: anubis.CookieName,
|
||||
Value: tokenString,
|
||||
Expires: time.Now().Add(24 * 7 * time.Hour),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Path: "/",
|
||||
Name: anubis.CookieName,
|
||||
Value: tokenString,
|
||||
Expires: time.Now().Add(24 * 7 * time.Hour),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Domain: s.opts.CookieDomain,
|
||||
Partitioned: s.opts.CookiePartitioned,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
challengesValidated.Inc()
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"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()
|
||||
|
||||
policy, err := LoadPoliciesOrDefault("", anubis.DefaultDifficulty)
|
||||
@@ -17,23 +20,102 @@ func spawnAnubis(t *testing.T, h http.Handler) string {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s, err := New(Options{
|
||||
Next: h,
|
||||
Policy: policy,
|
||||
ServeRobotsTXT: true,
|
||||
})
|
||||
return policy
|
||||
}
|
||||
|
||||
func spawnAnubis(t *testing.T, opts Options) *Server {
|
||||
t.Helper()
|
||||
|
||||
s, err := New(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(s)
|
||||
t.Log(ts.URL)
|
||||
return s
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
func TestCookieSettings(t *testing.T) {
|
||||
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) {
|
||||
|
||||
@@ -7,13 +7,14 @@ import (
|
||||
"github.com/TecharoHQ/anubis"
|
||||
)
|
||||
|
||||
func ClearCookie(w http.ResponseWriter) {
|
||||
func (s *Server) ClearCookie(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: anubis.CookieName,
|
||||
Value: "",
|
||||
Expires: time.Now().Add(-1 * time.Hour),
|
||||
MaxAge: -1,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Domain: s.opts.CookieDomain,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ templ base(title string, body templ.Component) {
|
||||
href="https://techaro.lol"
|
||||
>Techaro</a>. Made with ❤️ in 🇨🇦.
|
||||
</p>
|
||||
<p>Mascot design by <a href="https://bsky.app/profile/celphase.bsky.social">CELPHASE</a>.</p>
|
||||
</center>
|
||||
</footer>
|
||||
</main>
|
||||
@@ -208,7 +209,7 @@ templ errorPage(message string) {
|
||||
<img
|
||||
id="image"
|
||||
style="width:100%;max-width:256px;"
|
||||
src={ "/.within.website/x/cmd/anubis/static/img/sad.webp?cacheBuster=" + anubis.Version }
|
||||
src={ "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }
|
||||
/>
|
||||
<p>{ message }.</p>
|
||||
<button onClick="window.location.reload();">Try again</button>
|
||||
|
||||
@@ -89,7 +89,7 @@ func base(title string, body templ.Component) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<footer><center><p>Protected by <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> from <a href=\"https://techaro.lol\">Techaro</a>. Made with ❤️ in 🇨🇦.</p></center></footer></main></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<footer><center><p>Protected by <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> from <a href=\"https://techaro.lol\">Techaro</a>. Made with ❤️ in 🇨🇦.</p><p>Mascot design by <a href=\"https://bsky.app/profile/celphase.bsky.social\">CELPHASE</a>.</p></center></footer></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func index() templ.Component {
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
|
||||
anubis.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 169, Col: 18}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 170, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -140,7 +140,7 @@ func index() templ.Component {
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
|
||||
anubis.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 175, Col: 18}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 176, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -153,7 +153,7 @@ func index() templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 178, Col: 116}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 179, Col: 116}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -193,9 +193,9 @@ func errorPage(message string) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/sad.webp?cacheBuster=" + anubis.Version)
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 211, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 212, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -208,7 +208,7 @@ func errorPage(message string) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 213, Col: 14}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 214, Col: 14}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -27,6 +27,22 @@ const imageURL = (mood, cacheBuster) =>
|
||||
const spinner = document.getElementById('spinner');
|
||||
const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);
|
||||
|
||||
const ohNoes = ({ titleMsg, statusMsg, imageSrc }) => {
|
||||
title.innerHTML = titleMsg;
|
||||
status.innerHTML = statusMsg;
|
||||
image.src = imageSrc;
|
||||
progress.style.display = "none";
|
||||
};
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
ohNoes({
|
||||
titleMsg: "Your context is not secure!",
|
||||
statusMsg: `Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure">MDN</a>.`,
|
||||
imageSrc: imageURL("reject", anubisVersion),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// const testarea = document.getElementById('testarea');
|
||||
|
||||
// const videoWorks = await testVideo(testarea);
|
||||
@@ -35,9 +51,9 @@ const imageURL = (mood, cacheBuster) =>
|
||||
// if (!videoWorks) {
|
||||
// title.innerHTML = "Oh no!";
|
||||
// status.innerHTML = "Checks failed. Please check your browser's settings and try again.";
|
||||
// image.src = imageURL("sad");
|
||||
// spinner.innerHTML = "";
|
||||
// spinner.style.display = "none";
|
||||
// image.src = imageURL("reject");
|
||||
// return;
|
||||
// }
|
||||
|
||||
@@ -51,21 +67,21 @@ const imageURL = (mood, cacheBuster) =>
|
||||
return r.json();
|
||||
})
|
||||
.catch(err => {
|
||||
title.innerHTML = "Oh no!";
|
||||
status.innerHTML = `Failed to fetch config: ${err.message}`;
|
||||
image.src = imageURL("sad", anubisVersion);
|
||||
spinner.innerHTML = "";
|
||||
spinner.style.display = "none";
|
||||
ohNoes({
|
||||
titleMsg: "Internal error!",
|
||||
statusMsg: `Failed to fetch challenge config: ${err.message}`,
|
||||
imageSrc: imageURL("reject", anubisVersion),
|
||||
});
|
||||
throw err;
|
||||
});
|
||||
|
||||
const process = algorithms[rules.algorithm];
|
||||
if (!process) {
|
||||
title.innerHTML = "Oh no!";
|
||||
status.innerHTML = `Failed to resolve check algorithm. You may want to reload the page.`;
|
||||
image.src = imageURL("sad", anubisVersion);
|
||||
spinner.innerHTML = "";
|
||||
spinner.style.display = "none";
|
||||
ohNoes({
|
||||
titleMsg: "Challenge error!",
|
||||
statusMsg: `Failed to resolve check algorithm. You may want to reload the page.`,
|
||||
imageSrc: imageURL("reject", anubisVersion),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 28 KiB |
BIN
web/static/img/reject.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 50 KiB |
@@ -1,2 +1,2 @@
|
||||
(()=>{function p(r,n=5,t=navigator.hardwareConcurrency||1){return console.debug("fast algo"),new Promise((e,o)=>{let s=URL.createObjectURL(new Blob(["(",y(),")()"],{type:"application/javascript"})),a=[];for(let i=0;i<t;i++){let c=new Worker(s);c.onmessage=d=>{a.forEach(u=>u.terminate()),c.terminate(),e(d.data)},c.onerror=d=>{c.terminate(),o()},c.postMessage({data:r,difficulty:n,nonce:i,threads:t}),a.push(c)}URL.revokeObjectURL(s)})}function y(){return function(){let r=t=>{let e=new TextEncoder().encode(t);return crypto.subtle.digest("SHA-256",e.buffer)};function n(t){return Array.from(t).map(e=>e.toString(16).padStart(2,"0")).join("")}addEventListener("message",async t=>{let e=t.data.data,o=t.data.difficulty,s,a=t.data.nonce,i=t.data.threads;for(;;){let c=await r(e+a),d=new Uint8Array(c),u=!0;for(let m=0;m<o;m++){let l=Math.floor(m/2),g=m%2;if((d[l]>>(g===0?4:0)&15)!==0){u=!1;break}}if(u){s=n(d),console.log(s);break}a+=i}postMessage({hash:s,data:e,difficulty:o,nonce:a})})}.toString()}function f(r,n=5,t=1){return console.debug("slow algo"),new Promise((e,o)=>{let s=URL.createObjectURL(new Blob(["(",b(),")()"],{type:"application/javascript"})),a=new Worker(s);a.onmessage=i=>{a.terminate(),e(i.data)},a.onerror=i=>{a.terminate(),o()},a.postMessage({data:r,difficulty:n}),URL.revokeObjectURL(s)})}function b(){return function(){let r=n=>{let t=new TextEncoder().encode(n);return crypto.subtle.digest("SHA-256",t.buffer).then(e=>Array.from(new Uint8Array(e)).map(o=>o.toString(16).padStart(2,"0")).join(""))};addEventListener("message",async n=>{let t=n.data.data,e=n.data.difficulty,o,s=0;do o=await r(t+s++);while(o.substring(0,e)!==Array(e+1).join("0"));s-=1,postMessage({hash:o,data:t,difficulty:e,nonce:s})})}.toString()}var L={fast:p,slow:f},w=(r="",n={})=>{let t=new URL(r,window.location.href);return Object.entries(n).forEach(e=>{let[o,s]=e;t.searchParams.set(o,s)}),t.toString()},h=(r,n)=>w(`/.within.website/x/cmd/anubis/static/img/${r}.webp`,{cacheBuster:n});(async()=>{let r=document.getElementById("status"),n=document.getElementById("image"),t=document.getElementById("title"),e=document.getElementById("spinner"),o=JSON.parse(document.getElementById("anubis_version").textContent);r.innerHTML="Calculating...";let{challenge:s,rules:a}=await fetch("/.within.website/x/cmd/anubis/api/make-challenge",{method:"POST"}).then(l=>{if(!l.ok)throw new Error("Failed to fetch config");return l.json()}).catch(l=>{throw t.innerHTML="Oh no!",r.innerHTML=`Failed to fetch config: ${l.message}`,n.src=h("sad",o),e.innerHTML="",e.style.display="none",l}),i=L[a.algorithm];if(!i){t.innerHTML="Oh no!",r.innerHTML="Failed to resolve check algorithm. You may want to reload the page.",n.src=h("sad",o),e.innerHTML="",e.style.display="none";return}r.innerHTML=`Calculating...<br/>Difficulty: ${a.report_as}`;let c=Date.now(),{hash:d,nonce:u}=await i(s,a.difficulty),m=Date.now();console.log({hash:d,nonce:u}),t.innerHTML="Success!",r.innerHTML=`Done! Took ${m-c}ms, ${u} iterations`,n.src=h("happy",o),e.innerHTML="",e.style.display="none",setTimeout(()=>{let l=window.location.href;window.location.href=w("/.within.website/x/cmd/anubis/api/pass-challenge",{response:d,nonce:u,redir:l,elapsedTime:m-c})},250)})();})();
|
||||
(()=>{function p(s,r=5,e=navigator.hardwareConcurrency||1){return console.debug("fast algo"),new Promise((t,n)=>{let o=URL.createObjectURL(new Blob(["(",S(),")()"],{type:"application/javascript"})),a=[];for(let i=0;i<e;i++){let c=new Worker(o);c.onmessage=d=>{a.forEach(m=>m.terminate()),c.terminate(),t(d.data)},c.onerror=d=>{c.terminate(),n()},c.postMessage({data:s,difficulty:r,nonce:i,threads:e}),a.push(c)}URL.revokeObjectURL(o)})}function S(){return function(){let s=e=>{let t=new TextEncoder().encode(e);return crypto.subtle.digest("SHA-256",t.buffer)};function r(e){return Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}addEventListener("message",async e=>{let t=e.data.data,n=e.data.difficulty,o,a=e.data.nonce,i=e.data.threads;for(;;){let c=await s(t+a),d=new Uint8Array(c),m=!0;for(let u=0;u<n;u++){let g=Math.floor(u/2),l=u%2;if((d[g]>>(l===0?4:0)&15)!==0){m=!1;break}}if(m){o=r(d),console.log(o);break}a+=i}postMessage({hash:o,data:t,difficulty:n,nonce:a})})}.toString()}function f(s,r=5,e=1){return console.debug("slow algo"),new Promise((t,n)=>{let o=URL.createObjectURL(new Blob(["(",L(),")()"],{type:"application/javascript"})),a=new Worker(o);a.onmessage=i=>{a.terminate(),t(i.data)},a.onerror=i=>{a.terminate(),n()},a.postMessage({data:s,difficulty:r}),URL.revokeObjectURL(o)})}function L(){return function(){let s=r=>{let e=new TextEncoder().encode(r);return crypto.subtle.digest("SHA-256",e.buffer).then(t=>Array.from(new Uint8Array(t)).map(n=>n.toString(16).padStart(2,"0")).join(""))};addEventListener("message",async r=>{let e=r.data.data,t=r.data.difficulty,n,o=0;do n=await s(e+o++);while(n.substring(0,t)!==Array(t+1).join("0"));o-=1,postMessage({hash:n,data:e,difficulty:t,nonce:o})})}.toString()}var T={fast:p,slow:f},y=(s="",r={})=>{let e=new URL(s,window.location.href);return Object.entries(r).forEach(t=>{let[n,o]=t;e.searchParams.set(n,o)}),e.toString()},h=(s,r)=>y(`/.within.website/x/cmd/anubis/static/img/${s}.webp`,{cacheBuster:r});(async()=>{let s=document.getElementById("status"),r=document.getElementById("image"),e=document.getElementById("title"),t=document.getElementById("spinner"),n=JSON.parse(document.getElementById("anubis_version").textContent),o=({titleMsg:l,statusMsg:w,imageSrc:b})=>{e.innerHTML=l,s.innerHTML=w,r.src=b,progress.style.display="none"};if(!window.isSecureContext){o({titleMsg:"Your context is not secure!",statusMsg:'Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure">MDN</a>.',imageSrc:h("reject",n)});return}s.innerHTML="Calculating...";let{challenge:a,rules:i}=await fetch("/.within.website/x/cmd/anubis/api/make-challenge",{method:"POST"}).then(l=>{if(!l.ok)throw new Error("Failed to fetch config");return l.json()}).catch(l=>{throw o({titleMsg:"Internal error!",statusMsg:`Failed to fetch challenge config: ${l.message}`,imageSrc:h("reject",n)}),l}),c=T[i.algorithm];if(!c){o({titleMsg:"Challenge error!",statusMsg:"Failed to resolve check algorithm. You may want to reload the page.",imageSrc:h("reject",n)});return}s.innerHTML=`Calculating...<br/>Difficulty: ${i.report_as}`;let d=Date.now(),{hash:m,nonce:u}=await c(a,i.difficulty),g=Date.now();console.log({hash:m,nonce:u}),e.innerHTML="Success!",s.innerHTML=`Done! Took ${g-d}ms, ${u} iterations`,r.src=h("happy",n),t.innerHTML="",t.style.display="none",setTimeout(()=>{let l=window.location.href;window.location.href=y("/.within.website/x/cmd/anubis/api/pass-challenge",{response:m,nonce:u,redir:l,elapsedTime:g-d})},250)})();})();
|
||||
//# sourceMappingURL=main.mjs.map
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.850
|
||||
// templ: version: v0.3.857
|
||||
package xess
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||