Przeglądaj źródła

feat: add support for a base prefix (#294)

* fix: rename variable for preventing collision in ED25519 private key handling

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

* fix: remove unused import and debug print in xess.go

Signed-off-by: Jason Cameron <git@jasoncameron.dev>

* feat: introduce base path configuration for Anubis endpoints

Closes: #231
Signed-off-by: Jason Cameron <git@jasoncameron.dev>

* hack(internal/test): skip these tests for now

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

* fix(yeet): unbreak package builds

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

---------

Signed-off-by: Jason Cameron <git@jasoncameron.dev>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
Jason Cameron 1 miesiąc temu
rodzic
commit
24f8ba729b

+ 7 - 1
anubis.go

@@ -1,4 +1,4 @@
-// Package Anubis contains the version number of Anubis.
+// Package anubis contains the version number of Anubis.
 package anubis
 package anubis
 
 
 // Version is the current version of Anubis.
 // Version is the current version of Anubis.
@@ -11,9 +11,15 @@ var Version = "devel"
 // access.
 // access.
 const CookieName = "within.website-x-cmd-anubis-auth"
 const CookieName = "within.website-x-cmd-anubis-auth"
 
 
+// BasePrefix is a global prefix for all Anubis endpoints. Can be emptied to remove the prefix entirely.
+var BasePrefix = ""
+
 // StaticPath is the location where all static Anubis assets are located.
 // StaticPath is the location where all static Anubis assets are located.
 const StaticPath = "/.within.website/x/cmd/anubis/"
 const StaticPath = "/.within.website/x/cmd/anubis/"
 
 
+// APIPrefix is the location where all Anubis API endpoints are located.
+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

+ 20 - 12
cmd/anubis/main.go

@@ -38,6 +38,7 @@ import (
 )
 )
 
 
 var (
 var (
+	basePrefix               = flag.String("base-prefix", "", "base prefix (root URL) the application is served under e.g. /myapp")
 	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")
@@ -76,7 +77,7 @@ func keyFromHex(value string) (ed25519.PrivateKey, error) {
 }
 }
 
 
 func doHealthCheck() error {
 func doHealthCheck() error {
-	resp, err := http.Get("http://localhost" + *metricsBind + "/metrics")
+	resp, err := http.Get("http://localhost" + *metricsBind + anubis.BasePrefix + "/metrics")
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("failed to fetch metrics: %w", err)
 		return fmt.Errorf("failed to fetch metrics: %w", err)
 	}
 	}
@@ -178,13 +179,6 @@ func main() {
 
 
 	internal.InitSlog(*slogLevel)
 	internal.InitSlog(*slogLevel)
 
 
-	if *healthcheck {
-		if err := doHealthCheck(); err != nil {
-			log.Fatal(err)
-		}
-		return
-	}
-
 	if *extractResources != "" {
 	if *extractResources != "" {
 		if err := extractEmbedFS(data.BotPolicies, ".", *extractResources); err != nil {
 		if err := extractEmbedFS(data.BotPolicies, ".", *extractResources); err != nil {
 			log.Fatal(err)
 			log.Fatal(err)
@@ -230,6 +224,11 @@ func main() {
 			Action: config.RuleBenchmark,
 			Action: config.RuleBenchmark,
 		}}
 		}}
 	}
 	}
+	if *basePrefix != "" && !strings.HasPrefix(*basePrefix, "/") {
+		log.Fatalf("[misconfiguration] base-prefix must start with a slash, eg: /%s", *basePrefix)
+	} else if strings.HasSuffix(*basePrefix, "/") {
+		log.Fatalf("[misconfiguration] base-prefix must not end with a slash")
+	}
 
 
 	var priv ed25519.PrivateKey
 	var priv ed25519.PrivateKey
 	if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
 	if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
@@ -240,12 +239,12 @@ func main() {
 			log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err)
 			log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err)
 		}
 		}
 	} else if *ed25519PrivateKeyHexFile != "" {
 	} else if *ed25519PrivateKeyHexFile != "" {
-		hexData, err := os.ReadFile(*ed25519PrivateKeyHexFile)
+		hexFile, err := os.ReadFile(*ed25519PrivateKeyHexFile)
 		if err != nil {
 		if err != nil {
 			log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err)
 			log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err)
 		}
 		}
 
 
-		priv, err = keyFromHex(string(bytes.TrimSpace(hexData)))
+		priv, err = keyFromHex(string(bytes.TrimSpace(hexFile)))
 		if err != nil {
 		if err != nil {
 			log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err)
 			log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err)
 		}
 		}
@@ -273,6 +272,7 @@ func main() {
 	}
 	}
 
 
 	s, err := libanubis.New(libanubis.Options{
 	s, err := libanubis.New(libanubis.Options{
+		BasePrefix:        *basePrefix,
 		Next:              rp,
 		Next:              rp,
 		Policy:            policy,
 		Policy:            policy,
 		ServeRobotsTXT:    *robotsTxt,
 		ServeRobotsTXT:    *robotsTxt,
@@ -298,7 +298,6 @@ func main() {
 		wg.Add(1)
 		wg.Add(1)
 		go metricsServer(ctx, wg.Done)
 		go metricsServer(ctx, wg.Done)
 	}
 	}
-
 	go startDecayMapCleanup(ctx, s)
 	go startDecayMapCleanup(ctx, s)
 
 
 	var h http.Handler
 	var h http.Handler
@@ -320,6 +319,7 @@ func main() {
 		"debug-benchmark-js", *debugBenchmarkJS,
 		"debug-benchmark-js", *debugBenchmarkJS,
 		"og-passthrough", *ogPassthrough,
 		"og-passthrough", *ogPassthrough,
 		"og-expiry-time", *ogTimeToLive,
 		"og-expiry-time", *ogTimeToLive,
+		"base-prefix", *basePrefix,
 	)
 	)
 
 
 	go func() {
 	go func() {
@@ -341,12 +341,20 @@ func metricsServer(ctx context.Context, done func()) {
 	defer done()
 	defer done()
 
 
 	mux := http.NewServeMux()
 	mux := http.NewServeMux()
-	mux.Handle("/metrics", promhttp.Handler())
+	mux.Handle(anubis.BasePrefix+"/metrics", promhttp.Handler())
 
 
 	srv := http.Server{Handler: mux}
 	srv := http.Server{Handler: mux}
 	listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
 	listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
 	slog.Debug("listening for metrics", "url", metricsUrl)
 	slog.Debug("listening for metrics", "url", metricsUrl)
 
 
+	if *healthcheck {
+		log.Println("running healthcheck")
+		if err := doHealthCheck(); err != nil {
+			log.Fatal(err)
+		}
+		return
+	}
+
 	go func() {
 	go func() {
 		<-ctx.Done()
 		<-ctx.Done()
 		c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		c, cancel := context.WithTimeout(context.Background(), 5*time.Second)

+ 2 - 0
docs/docs/CHANGELOG.md

@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added example nginx configuration to documentation
 - Added example nginx configuration to documentation
 - Added example Apache configuration to the documentation [#277](https://github.com/TecharoHQ/anubis/issues/277)
 - Added example Apache configuration to the documentation [#277](https://github.com/TecharoHQ/anubis/issues/277)
 - Move per-environment configuration details into their own pages
 - Move per-environment configuration details into their own pages
+- Added support for running anubis behind a prefix (e.g. `/myapp`)
 - Added headers support to bot policy rules
 - Added headers support to bot policy rules
 - Moved configuration file from JSON to YAML by default
 - Moved configuration file from JSON to YAML by default
 - Added documentation on how to use Anubis with Traefik in Docker
 - Added documentation on how to use Anubis with Traefik in Docker
@@ -35,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Moved all CSS inline to the Xess package, changed colors to be CSS variables
 - Moved all CSS inline to the Xess package, changed colors to be CSS variables
 - Set or append to `X-Forwarded-For` header unless the remote connects over a loopback address [#328](https://github.com/TecharoHQ/anubis/issues/328)
 - Set or append to `X-Forwarded-For` header unless the remote connects over a loopback address [#328](https://github.com/TecharoHQ/anubis/issues/328)
 - Fixed mojeekbot user agent regex
 - Fixed mojeekbot user agent regex
+- Added support for running anubis behind a base path (e.g. `/myapp`)
 
 
 ## v1.16.0
 ## v1.16.0
 
 

+ 37 - 0
docs/docs/admin/installation.mdx

@@ -51,6 +51,7 @@ Anubis uses these environment variables for configuration:
 
 
 | Environment Variable           | Default value           | Explanation                                                                                                                                                                                                                                                                                                          |
 | Environment Variable           | Default value           | Explanation                                                                                                                                                                                                                                                                                                          |
 | :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `BASE_PREFIX`                  | unset                   | If set, adds a global prefix to all Anubis endpoints. 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.                                                                                                                                                                           |
 | `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_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.                                           |
@@ -72,6 +73,42 @@ Anubis uses these environment variables for configuration:
 
 
 For more detailed information on configuring Open Graph tags, please refer to the [Open Graph Configuration](./configuration/open-graph.mdx) page.
 For more detailed information on configuring Open Graph tags, please refer to the [Open Graph Configuration](./configuration/open-graph.mdx) page.
 
 
+### Using Base Prefix
+
+The `BASE_PREFIX` environment variable allows you to run Anubis behind a path prefix. This is useful when:
+
+- You want to host multiple services on the same domain
+- You're using a reverse proxy that routes based on path prefixes
+- You need to integrate Anubis with an existing application structure
+
+For example, if you set `BASE_PREFIX=/myapp`, Anubis will:
+
+- Serve its challenge page at `/myapp/` instead of `/`
+- Serve its API endpoints at `/myapp/.within.website/x/cmd/anubis/api/` instead of `/.within.website/x/cmd/anubis/api/`
+- Serve its static assets at `/myapp/.within.website/x/cmd/anubis/` instead of `/.within.website/x/cmd/anubis/`
+
+When using this feature with a reverse proxy:
+
+1. Configure your reverse proxy to route requests for the specified path prefix to Anubis
+2. Set the `BASE_PREFIX` environment variable to match the path prefix in your reverse proxy configuration
+3. Ensure that your reverse proxy preserves the path when forwarding requests to Anubis
+
+Example with Nginx:
+
+```nginx
+location /myapp/ {
+    proxy_pass http://anubis:8923/myapp;
+    proxy_set_header Host $host;
+    proxy_set_header X-Real-IP $remote_addr;
+}
+```
+
+With corresponding Anubis configuration:
+
+```
+BASE_PREFIX=/myapp
+```
+
 ### Key generation
 ### Key generation
 
 
 To generate an ed25519 private key, you can use this command:
 To generate an ed25519 private key, you can use this command:

+ 131 - 0
internal/test/playwright_test.go

@@ -265,6 +265,132 @@ func TestPlaywrightBrowser(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestPlaywrightWithBasePrefix(t *testing.T) {
+	if os.Getenv("DONT_USE_NETWORK") != "" {
+		t.Skip("test requires network egress")
+		return
+	}
+
+	t.Skip("NOTE(Xe)\\ these tests require HTTPS support in #364")
+
+	doesNPXExist(t)
+	startPlaywright(t)
+
+	pw := setupPlaywright(t)
+	basePrefix := "/myapp"
+	anubisURL := spawnAnubisWithOptions(t, basePrefix)
+
+	// Reset BasePrefix after test
+	t.Cleanup(func() {
+		anubis.BasePrefix = ""
+	})
+
+	browsers := []playwright.BrowserType{pw.Chromium}
+
+	for _, typ := range browsers {
+		t.Run(typ.Name()+"/basePrefix", func(t *testing.T) {
+			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)
+			}
+			defer browser.Close()
+
+			ctx, err := browser.NewContext(playwright.BrowserNewContextOptions{
+				AcceptDownloads: playwright.Bool(false),
+				ExtraHttpHeaders: map[string]string{
+					"X-Real-Ip": "127.0.0.1",
+				},
+				UserAgent: playwright.String("Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0"),
+			})
+			if err != nil {
+				t.Fatalf("could not create context: %v", err)
+			}
+			defer ctx.Close()
+
+			page, err := ctx.NewPage()
+			if err != nil {
+				t.Fatalf("could not create page: %v", err)
+			}
+			defer page.Close()
+
+			// Test accessing the base URL with prefix
+			_, err = page.Goto(anubisURL+basePrefix, playwright.PageGotoOptions{
+				Timeout: pwTimeout(testCases[0], time.Now().Add(5*time.Second)),
+			})
+			if err != nil {
+				pwFail(t, page, "could not navigate to test server with base prefix: %v", err)
+			}
+
+			// Check if challenge page is displayed
+			image := page.Locator("#image[src*=pensive], #image[src*=happy]")
+			err = image.WaitFor(playwright.LocatorWaitForOptions{
+				Timeout: pwTimeout(testCases[0], time.Now().Add(5*time.Second)),
+			})
+			if err != nil {
+				pwFail(t, page, "could not wait for challenge image: %v", err)
+			}
+
+			isVisible, err := image.IsVisible()
+			if err != nil {
+				pwFail(t, page, "could not check if challenge image is visible: %v", err)
+			}
+			if !isVisible {
+				pwFail(t, page, "challenge image not visible")
+			}
+
+			// Complete the challenge
+			// Wait for the challenge to be solved
+			anubisTest := page.Locator("#anubis-test")
+			err = anubisTest.WaitFor(playwright.LocatorWaitForOptions{
+				Timeout: pwTimeout(testCases[0], time.Now().Add(30*time.Second)),
+			})
+			if err != nil {
+				pwFail(t, page, "could not wait for challenge to be solved: %v", err)
+			}
+
+			// Verify the challenge was solved
+			content, err := anubisTest.TextContent(playwright.LocatorTextContentOptions{})
+			if err != nil {
+				pwFail(t, page, "could not get text content: %v", err)
+			}
+
+			var tm int64
+			if _, err := fmt.Sscanf(content, "%d", &tm); err != nil {
+				pwFail(t, page, "unexpected output: %s", content)
+			}
+
+			// Check if the timestamp is reasonable
+			now := time.Now().Unix()
+			if tm < now-60 || tm > now+60 {
+				pwFail(t, page, "unexpected timestamp in output: %d not in range %d±60", tm, now)
+			}
+
+			// Check if cookie has the correct path
+			cookies, err := ctx.Cookies()
+			if err != nil {
+				pwFail(t, page, "could not get cookies: %v", err)
+			}
+
+			var found bool
+			for _, cookie := range cookies {
+				if cookie.Name == anubis.CookieName {
+					found = true
+					if cookie.Path != basePrefix+"/" {
+						t.Errorf("cookie path is wrong, wanted %s, got: %s", basePrefix+"/", cookie.Path)
+					}
+					break
+				}
+			}
+
+			if !found {
+				t.Errorf("Cookie %q not found", anubis.CookieName)
+			}
+		})
+	}
+}
+
 func buildBrowserConnect(name string) string {
 func buildBrowserConnect(name string) string {
 	u, _ := url.Parse(*playwrightServer)
 	u, _ := url.Parse(*playwrightServer)
 
 
@@ -431,6 +557,10 @@ func setupPlaywright(t *testing.T) *playwright.Playwright {
 }
 }
 
 
 func spawnAnubis(t *testing.T) string {
 func spawnAnubis(t *testing.T) string {
+	return spawnAnubisWithOptions(t, "")
+}
+
+func spawnAnubisWithOptions(t *testing.T, basePrefix string) string {
 	t.Helper()
 	t.Helper()
 
 
 	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -457,6 +587,7 @@ func spawnAnubis(t *testing.T) string {
 		Policy:         policy,
 		Policy:         policy,
 		ServeRobotsTXT: true,
 		ServeRobotsTXT: true,
 		Target:         "http://" + host + ":" + port,
 		Target:         "http://" + host + ":" + port,
+		BasePrefix:     basePrefix,
 	})
 	})
 	if err != nil {
 	if err != nil {
 		t.Fatalf("can't construct libanubis.Server: %v", err)
 		t.Fatalf("can't construct libanubis.Server: %v", err)

+ 39 - 15
lib/anubis.go

@@ -80,6 +80,7 @@ type Options struct {
 	Target        string
 	Target        string
 
 
 	WebmasterEmail string
 	WebmasterEmail string
+	BasePrefix     string
 }
 }
 
 
 func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
 func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
@@ -121,6 +122,8 @@ func New(opts Options) (*Server, error) {
 		opts.PrivateKey = priv
 		opts.PrivateKey = priv
 	}
 	}
 
 
+	anubis.BasePrefix = opts.BasePrefix
+
 	result := &Server{
 	result := &Server{
 		next:       opts.Next,
 		next:       opts.Next,
 		priv:       opts.PrivateKey,
 		priv:       opts.PrivateKey,
@@ -134,26 +137,42 @@ func New(opts Options) (*Server, error) {
 	mux := http.NewServeMux()
 	mux := http.NewServeMux()
 	xess.Mount(mux)
 	xess.Mount(mux)
 
 
-	mux.Handle(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(anubis.StaticPath, http.FileServerFS(web.Static)))))
+	// Helper to add global prefix
+	registerWithPrefix := func(pattern string, handler http.Handler, method string) {
+		if method != "" {
+			method = method + " " // methods must end with a space to register with them
+		}
 
 
-	if opts.ServeRobotsTXT {
-		mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
-			http.ServeFileFS(w, r, web.Static, "static/robots.txt")
-		})
+		// Ensure there's no double slash when concatenating BasePrefix and pattern
+		basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
+		prefix := method + basePrefix
 
 
-		mux.HandleFunc("/.well-known/robots.txt", func(w http.ResponseWriter, r *http.Request) {
-			http.ServeFileFS(w, r, web.Static, "static/robots.txt")
-		})
+		// If pattern doesn't start with a slash, add one
+		if !strings.HasPrefix(pattern, "/") {
+			pattern = "/" + pattern
+		}
+
+		mux.Handle(prefix+pattern, handler)
 	}
 	}
 
 
-	// mux.HandleFunc("GET /.within.website/x/cmd/anubis/static/js/main.mjs", serveMainJSWithBestEncoding)
+	// Ensure there's no double slash when concatenating BasePrefix and StaticPath
+	stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
+	registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
 
 
-	mux.HandleFunc("POST /.within.website/x/cmd/anubis/api/make-challenge", result.MakeChallenge)
-	mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/pass-challenge", result.PassChallenge)
-	mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/check", result.maybeReverseProxyHttpStatusOnly)
-	mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/test-error", result.TestError)
+	if opts.ServeRobotsTXT {
+		registerWithPrefix("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			http.ServeFileFS(w, r, web.Static, "static/robots.txt")
+		}), "GET")
+		registerWithPrefix("/.well-known/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			http.ServeFileFS(w, r, web.Static, "static/robots.txt")
+		}), "GET")
+	}
 
 
-	mux.HandleFunc("/", result.maybeReverseProxyOrPage)
+	registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
+	registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
+	registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
+	registerWithPrefix(anubis.APIPrefix+"test-error", http.HandlerFunc(result.TestError), "GET")
+	registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
 
 
 	result.mux = mux
 	result.mux = mux
 
 
@@ -561,6 +580,11 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	// Adjust cookie path if base prefix is not empty
+	cookiePath := "/"
+	if anubis.BasePrefix != "" {
+		cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
+	}
 	// generate JWT cookie
 	// generate JWT cookie
 	token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
 	token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
 		"challenge": challenge,
 		"challenge": challenge,
@@ -585,7 +609,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 		SameSite:    http.SameSiteLaxMode,
 		SameSite:    http.SameSiteLaxMode,
 		Domain:      s.opts.CookieDomain,
 		Domain:      s.opts.CookieDomain,
 		Partitioned: s.opts.CookiePartitioned,
 		Partitioned: s.opts.CookiePartitioned,
-		Path:        "/",
+		Path:        cookiePath,
 	})
 	})
 
 
 	challengesValidated.Inc()
 	challengesValidated.Inc()

+ 139 - 0
lib/anubis_test.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httptest"
 	"os"
 	"os"
+	"strings"
 	"testing"
 	"testing"
 
 
 	"github.com/TecharoHQ/anubis"
 	"github.com/TecharoHQ/anubis"
@@ -254,3 +255,141 @@ func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
+
+func TestBasePrefix(t *testing.T) {
+	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintln(w, "OK")
+	})
+
+	testCases := []struct {
+		name       string
+		basePrefix string
+		path       string
+		expected   string
+	}{
+		{
+			name:       "no prefix",
+			basePrefix: "",
+			path:       "/.within.website/x/cmd/anubis/api/make-challenge",
+			expected:   "/.within.website/x/cmd/anubis/api/make-challenge",
+		},
+		{
+			name:       "with prefix",
+			basePrefix: "/myapp",
+			path:       "/myapp/.within.website/x/cmd/anubis/api/make-challenge",
+			expected:   "/myapp/.within.website/x/cmd/anubis/api/make-challenge",
+		},
+		{
+			name:       "with prefix and trailing slash",
+			basePrefix: "/myapp/",
+			path:       "/myapp/.within.website/x/cmd/anubis/api/make-challenge",
+			expected:   "/myapp/.within.website/x/cmd/anubis/api/make-challenge",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Reset the global BasePrefix before each test
+			anubis.BasePrefix = ""
+
+			pol := loadPolicies(t, "")
+			pol.DefaultDifficulty = 4
+
+			srv := spawnAnubis(t, Options{
+				Next:       h,
+				Policy:     pol,
+				BasePrefix: tc.basePrefix,
+			})
+
+			ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
+			defer ts.Close()
+
+			// Test API endpoint with prefix
+			resp, err := ts.Client().Post(ts.URL+tc.path, "", nil)
+			if err != nil {
+				t.Fatalf("can't request challenge: %v", err)
+			}
+			defer resp.Body.Close()
+
+			if resp.StatusCode != http.StatusOK {
+				t.Errorf("expected status code %d, got: %d", http.StatusOK, resp.StatusCode)
+			}
+
+			var chall challenge
+			if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
+				t.Fatalf("can't read challenge response body: %v", err)
+			}
+
+			if chall.Challenge == "" {
+				t.Errorf("expected non-empty challenge")
+			}
+
+			// Test cookie path when passing challenge
+			// Find a nonce that produces a hash with the required number of leading zeros
+			nonce := 0
+			var calculated string
+			for {
+				calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
+				calculated = internal.SHA256sum(calcString)
+				if strings.HasPrefix(calculated, strings.Repeat("0", pol.DefaultDifficulty)) {
+					break
+				}
+				nonce++
+			}
+			elapsedTime := 420
+			redir := "/"
+
+			cli := ts.Client()
+			cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+				return http.ErrUseLastResponse
+			}
+
+			// Construct the correct path for pass-challenge
+			passChallengePath := tc.path
+			passChallengePath = passChallengePath[:strings.LastIndex(passChallengePath, "/")+1] + "pass-challenge"
+
+			req, err := http.NewRequest(http.MethodGet, ts.URL+passChallengePath, 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: %v", err)
+			}
+
+			if resp.StatusCode != http.StatusFound {
+				t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
+			}
+
+			// Check cookie path
+			var ckie *http.Cookie
+			for _, cookie := range resp.Cookies() {
+				if cookie.Name == anubis.CookieName {
+					ckie = cookie
+					break
+				}
+			}
+			if ckie == nil {
+				t.Errorf("Cookie %q not found", anubis.CookieName)
+				return
+			}
+
+			expectedPath := "/"
+			if tc.basePrefix != "" {
+				expectedPath = strings.TrimSuffix(tc.basePrefix, "/") + "/"
+			}
+
+			if ckie.Path != expectedPath {
+				t.Errorf("cookie path is wrong, wanted %s, got: %s", expectedPath, ckie.Path)
+			}
+		})
+	}
+}

+ 54 - 30
web/index.templ

@@ -10,16 +10,56 @@ templ base(title string, body templ.Component, challenge any, ogTags map[string]
 	<html lang="en">
 	<html lang="en">
 		<head>
 		<head>
 			<title>{ title }</title>
 			<title>{ title }</title>
-			<link rel="stylesheet" href={ xess.URL }/>
+			<link rel="stylesheet" href={ anubis.BasePrefix + xess.URL }/>
 			<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
 			<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
 			<meta name="robots" content="noindex,nofollow"/>
 			<meta name="robots" content="noindex,nofollow"/>
 			for key, value := range ogTags {
 			for key, value := range ogTags {
 				<meta property={ key } content={ value }/>
 				<meta property={ key } content={ value }/>
 			}
 			}
+			<style>
+        body,
+        html {
+            height: 100%;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            margin-left: auto;
+            margin-right: auto;
+        }
+
+        .centered-div {
+            text-align: center;
+        }
+
+        #status {
+            font-variant-numeric: tabular-nums;
+        }
+
+        #progress {
+            display: none;
+            width: min(20rem, 90%);
+            height: 2rem;
+            border-radius: 1rem;
+            overflow: hidden;
+            margin: 1rem 0 2rem;
+            outline-color: #b16286;
+            outline-offset: 2px;
+            outline-style: solid;
+            outline-width: 4px;
+        }
+
+        .bar-inner {
+            background-color: #b16286;
+            height: 100%;
+            width: 0;
+            transition: width 0.25s ease-in;
+        }
+    </style>
 			@templ.JSONScript("anubis_version", anubis.Version)
 			@templ.JSONScript("anubis_version", anubis.Version)
 			if challenge != nil {
 			if challenge != nil {
 				@templ.JSONScript("anubis_challenge", challenge)
 				@templ.JSONScript("anubis_challenge", challenge)
 			}
 			}
+			@templ.JSONScript("anubis_base_prefix", anubis.BasePrefix)
 		</head>
 		</head>
 		<body id="top">
 		<body id="top">
 			<main>
 			<main>
@@ -44,20 +84,10 @@ templ base(title string, body templ.Component, challenge any, ogTags map[string]
 
 
 templ index() {
 templ index() {
 	<div class="centered-div">
 	<div class="centered-div">
-		<img
-			id="image"
-			style="width:100%;max-width:256px;"
-			src={ "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
-    anubis.Version }
-		/>
-		<img
-			style="display:none;"
-			style="width:100%;max-width:256px;"
-			src={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
-    anubis.Version }
-		/>
+		<img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
+		<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
 		<p id="status">Loading...</p>
 		<p id="status">Loading...</p>
-		<script async type="module" src={ "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
+		<script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
 		<div id="progress" role="progressbar" aria-labelledby="status">
 		<div id="progress" role="progressbar" aria-labelledby="status">
 			<div class="bar-inner"></div>
 			<div class="bar-inner"></div>
 		</div>
 		</div>
@@ -74,7 +104,9 @@ templ index() {
 				resources inaccessible for everyone.
 				resources inaccessible for everyone.
 			</p>
 			</p>
 			<p>
 			<p>
-				Anubis is a compromise. Anubis uses a <a href="https://anubis.techaro.lol/docs/design/why-proof-of-work">Proof-of-Work</a>
+				Anubis is a compromise. Anubis uses a <a
+	href="https://anubis.techaro.lol/docs/design/why-proof-of-work"
+>Proof-of-Work</a>
 				scheme in the vein of <a href="https://en.wikipedia.org/wiki/Hashcash">Hashcash</a>, a proposed
 				scheme in the vein of <a href="https://en.wikipedia.org/wiki/Hashcash">Hashcash</a>, a proposed
 				proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is
 				proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is
 				ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.
 				ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.
@@ -105,17 +137,12 @@ templ index() {
 
 
 templ errorPage(message string, mail string) {
 templ errorPage(message string, mail string) {
 	<div class="centered-div">
 	<div class="centered-div">
-		<img
-			id="image"
-			alt="Sad Anubis"
-			style="width:100%;max-width:256px;"
-			src={ "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }
-		/>
+		<img id="image" alt="Sad Anubis" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }/>
 		<p>{ message }.</p>
 		<p>{ message }.</p>
 		<button onClick="window.location.reload();">Try again</button>
 		<button onClick="window.location.reload();">Try again</button>
 		if mail != "" {
 		if mail != "" {
 			<p>
 			<p>
-				<a href="/">Go home</a> or if you believe you should not be blocked, please contact the webmaster at 
+				<a href="/">Go home</a> or if you believe you should not be blocked, please contact the webmaster at
 				<a href={ "mailto:" + templ.SafeURL(mail) }>
 				<a href={ "mailto:" + templ.SafeURL(mail) }>
 					{ mail }
 					{ mail }
 				</a>
 				</a>
@@ -141,7 +168,9 @@ templ StaticHappy() {
 templ bench() {
 templ bench() {
 	<div style="height:20rem;display:flex">
 	<div style="height:20rem;display:flex">
 		<table style="margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem">
 		<table style="margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem">
-			<thead style="border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1">
+			<thead
+				style="border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1"
+			>
 				<tr id="table-header" style="display:contents">
 				<tr id="table-header" style="display:contents">
 					<th style="width:4.5rem">Time</th>
 					<th style="width:4.5rem">Time</th>
 					<th style="width:4rem">Iters</th>
 					<th style="width:4rem">Iters</th>
@@ -159,14 +188,9 @@ templ bench() {
 			></tbody>
 			></tbody>
 		</table>
 		</table>
 		<div class="centered-div">
 		<div class="centered-div">
-			<img
-				id="image"
-				style="width:100%;max-width:256px;"
-				src={ "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
-        anubis.Version }
-			/>
+			<img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
 			<p id="status" style="max-width:256px">Loading...</p>
 			<p id="status" style="max-width:256px">Loading...</p>
-			<script async type="module" src={ "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version }></script>
+			<script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version }></script>
 			<div id="sparkline"></div>
 			<div id="sparkline"></div>
 			<noscript>
 			<noscript>
 				<p>Running the benchmark tool requires JavaScript to be enabled.</p>
 				<p>Running the benchmark tool requires JavaScript to be enabled.</p>

+ 46 - 41
web/index_templ.go

@@ -52,9 +52,9 @@ func base(title string, body templ.Component, challenge any, ogTags map[string]s
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var3 string
 		var templ_7745c5c3_Var3 string
-		templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(xess.URL)
+		templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + xess.URL)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 13, Col: 41}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 13, Col: 61}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
@@ -96,6 +96,10 @@ func base(title string, body templ.Component, challenge any, ogTags map[string]s
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
 		}
 		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<style>\n        body,\n        html {\n            height: 100%;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n            margin-left: auto;\n            margin-right: auto;\n        }\n\n        .centered-div {\n            text-align: center;\n        }\n\n        #status {\n            font-variant-numeric: tabular-nums;\n        }\n\n        #progress {\n            display: none;\n            width: min(20rem, 90%);\n            height: 2rem;\n            border-radius: 1rem;\n            overflow: hidden;\n            margin: 1rem 0 2rem;\n            outline-color: #b16286;\n            outline-offset: 2px;\n            outline-style: solid;\n            outline-width: 4px;\n        }\n\n        .bar-inner {\n            background-color: #b16286;\n            height: 100%;\n            width: 0;\n            transition: width 0.25s ease-in;\n        }\n    </style>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
 		templ_7745c5c3_Err = templ.JSONScript("anubis_version", anubis.Version).Render(ctx, templ_7745c5c3_Buffer)
 		templ_7745c5c3_Err = templ.JSONScript("anubis_version", anubis.Version).Render(ctx, templ_7745c5c3_Buffer)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
@@ -106,20 +110,24 @@ func base(title string, body templ.Component, challenge any, ogTags map[string]s
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</head><body id=\"top\"><main><center><h1 id=\"title\" class=\".centered-div\">")
+		templ_7745c5c3_Err = templ.JSONScript("anubis_base_prefix", anubis.BasePrefix).Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</head><body id=\"top\"><main><center><h1 id=\"title\" class=\".centered-div\">")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var6 string
 		var templ_7745c5c3_Var6 string
 		templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title)
 		templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 27, Col: 49}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 67, Col: 49}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h1></center>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h1></center>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
@@ -127,7 +135,7 @@ func base(title string, body templ.Component, challenge any, ogTags map[string]s
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<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>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<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 {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
@@ -156,48 +164,46 @@ func index() templ.Component {
 			templ_7745c5c3_Var7 = templ.NopComponent
 			templ_7745c5c3_Var7 = templ.NopComponent
 		}
 		}
 		ctx = templ.ClearChildren(ctx)
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var8 string
 		var templ_7745c5c3_Var8 string
-		templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
-			anubis.Version)
+		templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 51, Col: 18}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 87, Col: 165}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"> <img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"> <img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var9 string
 		var templ_7745c5c3_Var9 string
-		templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
-			anubis.Version)
+		templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 57, Col: 18}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 88, Col: 174}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><p id=\"status\">Loading...</p><script async type=\"module\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><p id=\"status\">Loading...</p><script async type=\"module\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var10 string
 		var templ_7745c5c3_Var10 string
-		templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
+		templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 60, Col: 116}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 90, Col: 136}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></script><div id=\"progress\" role=\"progressbar\" aria-labelledby=\"status\"><div class=\"bar-inner\"></div></div><details><summary>Why am I seeing this?</summary><p>You are seeing this because the administrator of this website has set up <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> to protect the server against the scourge of <a href=\"https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/\">AI companies aggressively scraping websites</a>. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.</p><p>Anubis is a compromise. Anubis uses a <a href=\"https://anubis.techaro.lol/docs/design/why-proof-of-work\">Proof-of-Work</a> scheme in the vein of <a href=\"https://en.wikipedia.org/wiki/Hashcash\">Hashcash</a>, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.</p><p>Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.</p><p>Please note that Anubis requires the use of modern JavaScript features that plugins like <a href=\"https://jshelter.org/\">JShelter</a> will disable. Please disable JShelter or other such plugins for this domain.</p></details><noscript><p>Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.</p></noscript><div id=\"testarea\"></div></div>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></script><div id=\"progress\" role=\"progressbar\" aria-labelledby=\"status\"><div class=\"bar-inner\"></div></div><details><summary>Why am I seeing this?</summary><p>You are seeing this because the administrator of this website has set up <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> to protect the server against the scourge of <a href=\"https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/\">AI companies aggressively scraping websites</a>. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.</p><p>Anubis is a compromise. Anubis uses a <a href=\"https://anubis.techaro.lol/docs/design/why-proof-of-work\">Proof-of-Work</a> scheme in the vein of <a href=\"https://en.wikipedia.org/wiki/Hashcash\">Hashcash</a>, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.</p><p>Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.</p><p>Please note that Anubis requires the use of modern JavaScript features that plugins like <a href=\"https://jshelter.org/\">JShelter</a> will disable. Please disable JShelter or other such plugins for this domain.</p></details><noscript><p>Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.</p></noscript><div id=\"testarea\"></div></div>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
@@ -226,38 +232,38 @@ func errorPage(message string, mail string) templ.Component {
 			templ_7745c5c3_Var11 = templ.NopComponent
 			templ_7745c5c3_Var11 = templ.NopComponent
 		}
 		}
 		ctx = templ.ClearChildren(ctx)
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"centered-div\"><img id=\"image\" alt=\"Sad Anubis\" style=\"width:100%;max-width:256px;\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"centered-div\"><img id=\"image\" alt=\"Sad Anubis\" style=\"width:100%;max-width:256px;\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var12 string
 		var templ_7745c5c3_Var12 string
-		templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version)
+		templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 112, Col: 93}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 140, Col: 181}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"><p>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><p>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var13 string
 		var templ_7745c5c3_Var13 string
 		templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(message)
 		templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(message)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 114, Col: 14}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 141, Col: 14}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ".</p><button onClick=\"window.location.reload();\">Try again</button> ")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ".</p><button onClick=\"window.location.reload();\">Try again</button> ")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		if mail != "" {
 		if mail != "" {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p><a href=\"/\">Go home</a> or if you believe you should not be blocked, please contact the webmaster at  <a href=\"")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p><a href=\"/\">Go home</a> or if you believe you should not be blocked, please contact the webmaster at <a href=\"")
 			if templ_7745c5c3_Err != nil {
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
@@ -266,30 +272,30 @@ func errorPage(message string, mail string) templ.Component {
 			if templ_7745c5c3_Err != nil {
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
 			if templ_7745c5c3_Err != nil {
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
 			var templ_7745c5c3_Var15 string
 			var templ_7745c5c3_Var15 string
 			templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(mail)
 			templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(mail)
 			if templ_7745c5c3_Err != nil {
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 120, Col: 11}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 147, Col: 11}
 			}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
 			if templ_7745c5c3_Err != nil {
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a></p>")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a></p>")
 			if templ_7745c5c3_Err != nil {
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
 		} else {
 		} else {
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p><a href=\"/\">Go home</a></p>")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p><a href=\"/\">Go home</a></p>")
 			if templ_7745c5c3_Err != nil {
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 				return templ_7745c5c3_Err
 			}
 			}
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
@@ -318,7 +324,7 @@ func StaticHappy() templ.Component {
 			templ_7745c5c3_Var16 = templ.NopComponent
 			templ_7745c5c3_Var16 = templ.NopComponent
 		}
 		}
 		ctx = templ.ClearChildren(ctx)
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"centered-div\"><img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"centered-div\"><img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
@@ -326,13 +332,13 @@ func StaticHappy() templ.Component {
 		templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
 		templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
 			anubis.Version)
 			anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 135, Col: 18}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 162, Col: 18}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><p>This is just a check endpoint for your reverse proxy to use.</p></div>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><p>This is just a check endpoint for your reverse proxy to use.</p></div>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
@@ -361,34 +367,33 @@ func bench() templ.Component {
 			templ_7745c5c3_Var18 = templ.NopComponent
 			templ_7745c5c3_Var18 = templ.NopComponent
 		}
 		}
 		ctx = templ.ClearChildren(ctx)
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var19 string
 		var templ_7745c5c3_Var19 string
-		templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
-			anubis.Version)
+		templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 166, Col: 22}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 191, Col: 166}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
 		var templ_7745c5c3_Var20 string
 		var templ_7745c5c3_Var20 string
-		templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
+		templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 169, Col: 118}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 193, Col: 138}
 		}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">Difficulty:</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">Algorithm:</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">Difficulty:</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">Algorithm:</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}

+ 10 - 9
web/js/main.mjs

@@ -14,8 +14,8 @@ const u = (url = "", params = {}) => {
   return result.toString();
   return result.toString();
 };
 };
 
 
-const imageURL = (mood, cacheBuster) =>
-  u(`/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });
+const imageURL = (mood, cacheBuster, basePrefix) =>
+  u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });
 
 
 const dependencies = [
 const dependencies = [
   {
   {
@@ -81,6 +81,7 @@ function showContinueBar(hash, nonce, t0, t1) {
   const title = document.getElementById('title');
   const title = document.getElementById('title');
   const progress = document.getElementById('progress');
   const progress = document.getElementById('progress');
   const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);
   const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);
+  const basePrefix = JSON.parse(document.getElementById('anubis_base_prefix').textContent);
   const details = document.querySelector('details');
   const details = document.querySelector('details');
   let userReadDetails = false;
   let userReadDetails = false;
 
 
@@ -103,7 +104,7 @@ function showContinueBar(hash, nonce, t0, t1) {
     ohNoes({
     ohNoes({
       titleMsg: "Your context is not secure!",
       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>.`,
       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),
+      imageSrc: imageURL("reject", anubisVersion, basePrefix),
     });
     });
     return;
     return;
   }
   }
@@ -128,7 +129,7 @@ function showContinueBar(hash, nonce, t0, t1) {
       ohNoes({
       ohNoes({
         titleMsg: `Missing feature ${name}`,
         titleMsg: `Missing feature ${name}`,
         statusMsg: msg,
         statusMsg: msg,
-        imageSrc: imageURL("reject", anubisVersion),
+        imageSrc: imageURL("reject", anubisVersion, basePrefix),
       });
       });
     }
     }
   }
   }
@@ -140,7 +141,7 @@ function showContinueBar(hash, nonce, t0, t1) {
     ohNoes({
     ohNoes({
       titleMsg: "Challenge error!",
       titleMsg: "Challenge error!",
       statusMsg: `Failed to resolve check algorithm. You may want to reload the page.`,
       statusMsg: `Failed to resolve check algorithm. You may want to reload the page.`,
-      imageSrc: imageURL("reject", anubisVersion),
+      imageSrc: imageURL("reject", anubisVersion, basePrefix),
     });
     });
     return;
     return;
   }
   }
@@ -198,7 +199,7 @@ function showContinueBar(hash, nonce, t0, t1) {
 
 
     title.innerHTML = "Success!";
     title.innerHTML = "Success!";
     status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;
     status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;
-    image.src = imageURL("happy", anubisVersion);
+    image.src = imageURL("happy", anubisVersion, basePrefix);
     progress.style.display = "none";
     progress.style.display = "none";
 
 
     if (userReadDetails) {
     if (userReadDetails) {
@@ -223,7 +224,7 @@ function showContinueBar(hash, nonce, t0, t1) {
       function onDetailsExpand() {
       function onDetailsExpand() {
         const redir = window.location.href;
         const redir = window.location.href;
         window.location.replace(
         window.location.replace(
-          u("/.within.website/x/cmd/anubis/api/pass-challenge", {
+          u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
             response: hash,
             response: hash,
             nonce,
             nonce,
             redir,
             redir,
@@ -239,7 +240,7 @@ function showContinueBar(hash, nonce, t0, t1) {
       setTimeout(() => {
       setTimeout(() => {
         const redir = window.location.href;
         const redir = window.location.href;
         window.location.replace(
         window.location.replace(
-          u("/.within.website/x/cmd/anubis/api/pass-challenge", {
+          u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
             response: hash,
             response: hash,
             nonce,
             nonce,
             redir,
             redir,
@@ -253,7 +254,7 @@ function showContinueBar(hash, nonce, t0, t1) {
     ohNoes({
     ohNoes({
       titleMsg: "Calculation error!",
       titleMsg: "Calculation error!",
       statusMsg: `Failed to calculate challenge: ${err.message}`,
       statusMsg: `Failed to calculate challenge: ${err.message}`,
-      imageSrc: imageURL("reject", anubisVersion),
+      imageSrc: imageURL("reject", anubisVersion, basePrefix),
     });
     });
   }
   }
 })();
 })();

+ 4 - 1
xess/xess.go

@@ -32,6 +32,9 @@ func init() {
 	URL = URL + "?cachebuster=" + anubis.Version
 	URL = URL + "?cachebuster=" + anubis.Version
 }
 }
 
 
+// Mount registers the xess static file handlers on the given mux
 func Mount(mux *http.ServeMux) {
 func Mount(mux *http.ServeMux) {
-	mux.Handle("/.within.website/x/xess/", internal.UnchangingCache(http.StripPrefix("/.within.website/x/xess/", http.FileServerFS(Static))))
+	prefix := anubis.BasePrefix + "/.within.website/x/xess/"
+
+	mux.Handle(prefix, internal.UnchangingCache(http.StripPrefix(prefix, http.FileServerFS(Static))))
 }
 }

+ 1 - 1
yeetfile.js

@@ -12,7 +12,7 @@ $`npm run assets`;
             "./README.md": "README.md",
             "./README.md": "README.md",
             "./LICENSE": "LICENSE",
             "./LICENSE": "LICENSE",
             "./docs/docs/CHANGELOG.md": "CHANGELOG.md",
             "./docs/docs/CHANGELOG.md": "CHANGELOG.md",
-            "./docs/docs/admin/policies.md": "policies.md",
+            "./docs/docs/admin/policies.mdx": "policies.md",
             "./docs/docs/admin/native-install.mdx": "native-install.mdx",
             "./docs/docs/admin/native-install.mdx": "native-install.mdx",
             "./data/botPolicies.json": "botPolicies.json",
             "./data/botPolicies.json": "botPolicies.json",
         },
         },