Repository: TecharoHQ/anubis

Issue:
have BASE_PREFIX but strip it from target?
sorry for the dumb question

without anubis, i have caddy configured like this

```
example.com {
    route /foo/* {
        uri strip_prefix /foo
        reverse_proxy http://foo_server
    }
    redir /foo /foo/
}
```

so example.com/foo/bar forwards the request to foo_server stripping /foo

with anubis, i set

```
BIND=:3000
TARGET=http://foo_server
```

and changed to `reverse_proxy http://anubis:3000`

but anubis sends HTML with `/.within.website/`, so i added anubis env

```
BASE_PREFIX=/foo
```

and dropped the `uri strip_prefix /foo` caddy line, so the challenge works, but the foo_server now receives /foo which it doesn't understand, what am it doing wrong

Hints from issue comments:
@JasonLovesDoggo: > and dropped the uri strip_prefix /foo caddy line, so the challenge works, but the foo_server now receives /foo which it doesn't understand, what am it doing wrong

Just to confirm, when you visit example.com/foo/bar foo_server receives... just `/foo`? or `/foo/bar`. Either way it should just be receiving `/bar`@hydrargyrum: It receives `/foo/bar`.
I guess anubis doesn't hide `/foo` from the target server even though `BASE_PREFIX=/foo` because the target server would kind of bypass anubis if for some reason it outputs absolute links like "/something". Without prior knowledge of its base URL (which must be configurable in the target server, but that's not anubis' scope), it can't do better. But if the target server knows its base URL, then it can output links like "/foo/something". If that's the rationale, then the problem is not fixable in anubis (or add a STRIP_BASE_PREFIX bool, but that may be too much)@JasonLovesDoggo: > It receives `/foo/bar`. I guess anubis doesn't hide `/foo` from the target server even though `BASE_PREFIX=/foo` because the target server would kind of bypass anubis if for some reason it outputs absolute links like "/something". Without prior knowledge of its base URL (which must be configurable in the target server, but that's not anubis' scope), it can't do better. But if the target server knows its base URL, then it can output links like "/foo/something". If that's the rationale, then the problem is not fixable in anubis (or add a STRIP_BASE_PREFIX bool, but that may be too much)

I think it's reasonable to add a `STRIP_BASE_PREFIX` bool. Anubis is at the day a reverse proxy so it's fair to support this.  

Solution patch:
diff --git a/.air.toml b/.air.toml
index 9fd7e9a..3809007 100644
--- a/.air.toml
+++ b/.air.toml
@@ -9,4 +9,4 @@ exclude_dir = ["var", "vendor", "docs", "node_modules"]
 
 [logger]
 time = true
-# to change flags at runtime, prepend with -- e.g. $ air -- --target http://localhost:3000 --difficulty 20 --use-remote-address
\ No newline at end of file
+# to change flags at runtime, prepend with -- e.g. $ air -- --target http://localhost:3000 --difficulty 20 --use-remote-address
diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go
index 92fe1a2..d67f414 100644
--- a/cmd/anubis/main.go
+++ b/cmd/anubis/main.go
@@ -55,6 +55,7 @@ var (
 	policyFname              = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)")
 	redirectDomains          = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
 	slogLevel                = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
+	stripBasePrefix          = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server")
 	target                   = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request")
 	targetSNI                = flag.String("target-sni", "", "if set, the value of the TLS handshake hostname when forwarding requests to the target")
 	targetHost               = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target")
@@ -260,6 +261,10 @@ func main() {
 	} else if strings.HasSuffix(*basePrefix, "/") {
 		log.Fatalf("[misconfiguration] base-prefix must not end with a slash")
 	}
+	if *stripBasePrefix && *basePrefix == "" {
+		log.Fatalf("[misconfiguration] strip-base-prefix is set to true, but base-prefix is not set, " +
+			"this may result in unexpected behavior")
+	}
 
 	var priv ed25519.PrivateKey
 	if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
@@ -304,6 +309,7 @@ func main() {
 
 	s, err := libanubis.New(libanubis.Options{
 		BasePrefix:           *basePrefix,
+		StripBasePrefix:      *stripBasePrefix,
 		Next:                 rp,
 		Policy:               policy,
 		ServeRobotsTXT:       *robotsTxt,
diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md
index 0011ba9..6be9207 100644
--- a/docs/docs/CHANGELOG.md
+++ b/docs/docs/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Implement a no-JS challenge method: [`metarefresh`](./admin/configuration/challenges/metarefresh.mdx) ([#95](https://github.com/TecharoHQ/anubis/issues/95))
 - Bump AI-robots.txt to version 1.34
 - Make progress bar styling more compatible (UXP, etc)
+- Add `--strip-base-prefix` flag/envvar to strip the base prefix from request paths when forwarding to target servers
 
 ## v1.19.1: Jenomis cen Lexentale - Echo 1
 
diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx
index 5992739..60f49fc 100644
--- a/docs/docs/admin/installation.mdx
+++ b/docs/docs/admin/installation.mdx
@@ -4,8 +4,6 @@ title: Setting up Anubis
 
 import RandomKey from "@site/src/components/RandomKey";
 
-import Tabs from "@theme/Tabs";
-import TabItem from "@theme/TabItem";
 
 Anubis is meant to sit between your reverse proxy (such as Nginx or Caddy) and your target service. One instance of Anubis must be used per service you are protecting.
 
@@ -32,7 +30,7 @@ TLS terminator)
 Anubis is shipped in the Docker repo [`ghcr.io/techarohq/anubis`](https://github.com/TecharoHQ/anubis/pkgs/container/anubis). The following tags exist for your convenience:
 
 | Tag                 | Meaning                                                                                                                            |
-| :------------------ | :--------------------------------------------------------------------------------------------------------------------------------- |
+|:--------------------|:-----------------------------------------------------------------------------------------------------------------------------------|
 | `latest`            | The latest [tagged release](https://github.com/TecharoHQ/anubis/releases), if you are in doubt, start here.                        |
 | `v<version number>` | The Anubis image for [any given tagged release](https://github.com/TecharoHQ/anubis/tags)                                          |
 | `main`              | The current build on the `main` branch. Only use this if you need the latest and greatest features as they are merged into `main`. |
@@ -50,7 +48,7 @@ For more detailed information on installing Anubis with native packages, please
 Anubis uses these environment variables for configuration:
 
 | 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_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.                                                                                                                                                                                                                                                                                                                                                              |
@@ -69,6 +67,7 @@ Anubis uses these environment variables for configuration:
 | `REDIRECT_DOMAINS`             | unset                   | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain.<br/><br/>Note that if you are hosting Anubis on a non-standard port (`https://example:com:8443`, `http://www.example.net:8080`, etc.), you must also include the port number here. |
 | `SERVE_ROBOTS_TXT`             | `false`                 | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file.                                                                                                                                                                                                                |
 | `SOCKET_MODE`                  | `0770`                  | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets.                                                                                                                                                                                                                                                                                                                                                               |
+| `STRIP_BASE_PREFIX`            | `false`                 | If set to `true`, strips the base prefix from request paths when forwarding to the target server. This is useful when your target service expects to receive requests without the base prefix. For example, with `BASE_PREFIX=/foo` and `STRIP_BASE_PREFIX=true`, a request to `/foo/bar` would be forwarded to the target as `/bar`.                                                                                                                                                                   |
 | `TARGET`                       | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`.                                                                                                                                                                                                                                                                                                                                            |
 | `USE_REMOTE_ADDRESS`           | unset                   | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead.                                                                                                                                                                                                                                                                                       |
 | `WEBMASTER_EMAIL`              | unset                   | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators.                                                                                                                                                                                                                                                                                                                                                          |
@@ -129,6 +128,21 @@ With corresponding Anubis configuration:
 BASE_PREFIX=/myapp
 ```
 
+#### Stripping Base Prefix
+
+If your target service doesn't expect to receive the base prefix in request paths, you can use the `STRIP_BASE_PREFIX` option:
+
+```
+BASE_PREFIX=/myapp
+STRIP_BASE_PREFIX=true
+```
+
+With this configuration:
+- A request to `/myapp/api/users` would be forwarded to your target service as `/api/users`
+- A request to `/myapp/` would be forwarded as `/`
+
+This is particularly useful when working with applications that weren't designed to handle path prefixes. However, note that if your target application generates absolute redirects or links (like `/login` instead of `./login`), these may break the subpath routing since they won't include the base prefix.
+
 ### Key generation
 
 To generate an ed25519 private key, you can use this command:
diff --git a/lib/challenge/error.go b/lib/challenge/error.go
index f968178..7a9d309 100644
--- a/lib/challenge/error.go
+++ b/lib/challenge/error.go
@@ -22,9 +22,9 @@ func NewError(verb, publicReason string, privateReason error) *Error {
 }
 
 type Error struct {
+	PrivateReason error
 	Verb          string
 	PublicReason  string
-	PrivateReason error
 	StatusCode    int
 }
 
diff --git a/lib/config.go b/lib/config.go
index 06d613c..47a1b6e 100644
--- a/lib/config.go
+++ b/lib/config.go
@@ -36,6 +36,7 @@ type Options struct {
 	PrivateKey           ed25519.PrivateKey
 	CookieExpiration     time.Duration
 	OGTimeToLive         time.Duration
+	StripBasePrefix      bool
 	OGCacheConsidersHost bool
 	OGPassthrough        bool
 	CookiePartitioned    bool
diff --git a/lib/http.go b/lib/http.go
index e2400a6..11bdd8e 100644
--- a/lib/http.go
+++ b/lib/http.go
@@ -134,6 +134,32 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	s.mux.ServeHTTP(w, r)
 }
 
+func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request {
+	if !s.opts.StripBasePrefix || s.opts.BasePrefix == "" {
+		return r
+	}
+
+	basePrefix := strings.TrimSuffix(s.opts.BasePrefix, "/")
+	path := r.URL.Path
+
+	if !strings.HasPrefix(path, basePrefix) {
+		return r
+	}
+
+	trimmedPath := strings.TrimPrefix(path, basePrefix)
+	if trimmedPath == "" {
+		trimmedPath = "/"
+	}
+
+	// Clone the request and URL
+	reqCopy := r.Clone(r.Context())
+	urlCopy := *r.URL
+	urlCopy.Path = trimmedPath
+	reqCopy.URL = &urlCopy
+
+	return reqCopy
+}
+
 func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
 	if s.next == nil {
 		redir := r.FormValue("redir")
@@ -158,6 +184,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
 		).ServeHTTP(w, r)
 	} else {
 		requestsProxied.WithLabelValues(r.Host).Inc()
+		r = s.stripBasePrefixFromRequest(r)
 		s.next.ServeHTTP(w, r)
 	}
 }
diff --git a/lib/policy/bot.go b/lib/policy/bot.go
index 6107524..831c737 100644
--- a/lib/policy/bot.go
+++ b/lib/policy/bot.go
@@ -10,9 +10,9 @@ import (
 type Bot struct {
 	Rules     Checker
 	Challenge *config.ChallengeRules
+	Weight    *config.Weight
 	Name      string
 	Action    config.Rule
-	Weight    *config.Weight
 }
 
 func (b Bot) Hash() string {



Test patch:
diff --git a/lib/anubis_test.go b/lib/anubis_test.go
index 45724c5..3b9a1b1 100644
--- a/lib/anubis_test.go
+++ b/lib/anubis_test.go
@@ -632,3 +632,102 @@ func TestRuleChange(t *testing.T) {
 		t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
 	}
 }
+
+func TestStripBasePrefixFromRequest(t *testing.T) {
+	testCases := []struct {
+		name            string
+		basePrefix      string
+		stripBasePrefix bool
+		requestPath     string
+		expectedPath    string
+	}{
+		{
+			name:            "strip disabled - no change",
+			basePrefix:      "/foo",
+			stripBasePrefix: false,
+			requestPath:     "/foo/bar",
+			expectedPath:    "/foo/bar",
+		},
+		{
+			name:            "strip enabled - removes prefix",
+			basePrefix:      "/foo",
+			stripBasePrefix: true,
+			requestPath:     "/foo/bar",
+			expectedPath:    "/bar",
+		},
+		{
+			name:            "strip enabled - root becomes slash",
+			basePrefix:      "/foo",
+			stripBasePrefix: true,
+			requestPath:     "/foo",
+			expectedPath:    "/",
+		},
+		{
+			name:            "strip enabled - trailing slash on base prefix",
+			basePrefix:      "/foo/",
+			stripBasePrefix: true,
+			requestPath:     "/foo/bar",
+			expectedPath:    "/bar",
+		},
+		{
+			name:            "strip enabled - no prefix match",
+			basePrefix:      "/foo",
+			stripBasePrefix: true,
+			requestPath:     "/other/bar",
+			expectedPath:    "/other/bar",
+		},
+		{
+			name:            "strip enabled - empty base prefix",
+			basePrefix:      "",
+			stripBasePrefix: true,
+			requestPath:     "/foo/bar",
+			expectedPath:    "/foo/bar",
+		},
+		{
+			name:            "strip enabled - nested path",
+			basePrefix:      "/app",
+			stripBasePrefix: true,
+			requestPath:     "/app/api/v1/users",
+			expectedPath:    "/api/v1/users",
+		},
+		{
+			name:            "strip enabled - exact match becomes root",
+			basePrefix:      "/myapp",
+			stripBasePrefix: true,
+			requestPath:     "/myapp/",
+			expectedPath:    "/",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			srv := &Server{
+				opts: Options{
+					BasePrefix:      tc.basePrefix,
+					StripBasePrefix: tc.stripBasePrefix,
+				},
+			}
+
+			req := httptest.NewRequest(http.MethodGet, tc.requestPath, nil)
+			originalPath := req.URL.Path
+
+			result := srv.stripBasePrefixFromRequest(req)
+
+			if result.URL.Path != tc.expectedPath {
+				t.Errorf("expected path %q, got %q", tc.expectedPath, result.URL.Path)
+			}
+
+			// Ensure original request is not modified when no stripping should occur
+			if !tc.stripBasePrefix || tc.basePrefix == "" || !strings.HasPrefix(tc.requestPath, strings.TrimSuffix(tc.basePrefix, "/")) {
+				if result != req {
+					t.Error("expected same request object when no modification needed")
+				}
+			} else {
+				// Ensure original request is not modified when stripping occurs
+				if req.URL.Path != originalPath {
+					t.Error("original request was modified")
+				}
+			}
+		})
+	}
+}