+1
-1
.air.toml
+1
-1
.air.toml
···
9
9
10
10
[logger]
11
11
time = true
12
-
# to change flags at runtime, prepend with -- e.g. $ air -- --target http://localhost:3000 --difficulty 20 --use-remote-address
12
+
# to change flags at runtime, prepend with -- e.g. $ air -- --target http://localhost:3000 --difficulty 20 --use-remote-address
+6
cmd/anubis/main.go
+6
cmd/anubis/main.go
···
55
55
policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)")
56
56
redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
57
57
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
58
+
stripBasePrefix = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server")
58
59
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")
59
60
targetSNI = flag.String("target-sni", "", "if set, the value of the TLS handshake hostname when forwarding requests to the target")
60
61
targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target")
···
260
261
} else if strings.HasSuffix(*basePrefix, "/") {
261
262
log.Fatalf("[misconfiguration] base-prefix must not end with a slash")
262
263
}
264
+
if *stripBasePrefix && *basePrefix == "" {
265
+
log.Fatalf("[misconfiguration] strip-base-prefix is set to true, but base-prefix is not set, " +
266
+
"this may result in unexpected behavior")
267
+
}
263
268
264
269
var priv ed25519.PrivateKey
265
270
if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
···
304
309
305
310
s, err := libanubis.New(libanubis.Options{
306
311
BasePrefix: *basePrefix,
312
+
StripBasePrefix: *stripBasePrefix,
307
313
Next: rp,
308
314
Policy: policy,
309
315
ServeRobotsTXT: *robotsTxt,
+1
docs/docs/CHANGELOG.md
+1
docs/docs/CHANGELOG.md
···
20
20
- Implement a no-JS challenge method: [`metarefresh`](./admin/configuration/challenges/metarefresh.mdx) ([#95](https://github.com/TecharoHQ/anubis/issues/95))
21
21
- Bump AI-robots.txt to version 1.34
22
22
- Make progress bar styling more compatible (UXP, etc)
23
+
- Add `--strip-base-prefix` flag/envvar to strip the base prefix from request paths when forwarding to target servers
23
24
24
25
## v1.19.1: Jenomis cen Lexentale - Echo 1
25
26
+18
-4
docs/docs/admin/installation.mdx
+18
-4
docs/docs/admin/installation.mdx
···
4
4
5
5
import RandomKey from "@site/src/components/RandomKey";
6
6
7
-
import Tabs from "@theme/Tabs";
8
-
import TabItem from "@theme/TabItem";
9
7
10
8
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.
11
9
···
32
30
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:
33
31
34
32
| Tag | Meaning |
35
-
| :------------------ | :--------------------------------------------------------------------------------------------------------------------------------- |
33
+
|:--------------------|:-----------------------------------------------------------------------------------------------------------------------------------|
36
34
| `latest` | The latest [tagged release](https://github.com/TecharoHQ/anubis/releases), if you are in doubt, start here. |
37
35
| `v<version number>` | The Anubis image for [any given tagged release](https://github.com/TecharoHQ/anubis/tags) |
38
36
| `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
48
Anubis uses these environment variables for configuration:
51
49
52
50
| Environment Variable | Default value | Explanation |
53
-
| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
51
+
|:-------------------------------|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
54
52
| `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. |
55
53
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
56
54
| `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
67
| `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. |
70
68
| `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. |
71
69
| `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. |
70
+
| `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`. |
72
71
| `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`. |
73
72
| `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. |
74
73
| `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. |
···
128
127
```
129
128
BASE_PREFIX=/myapp
130
129
```
130
+
131
+
#### Stripping Base Prefix
132
+
133
+
If your target service doesn't expect to receive the base prefix in request paths, you can use the `STRIP_BASE_PREFIX` option:
134
+
135
+
```
136
+
BASE_PREFIX=/myapp
137
+
STRIP_BASE_PREFIX=true
138
+
```
139
+
140
+
With this configuration:
141
+
- A request to `/myapp/api/users` would be forwarded to your target service as `/api/users`
142
+
- A request to `/myapp/` would be forwarded as `/`
143
+
144
+
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.
131
145
132
146
### Key generation
133
147
+99
lib/anubis_test.go
+99
lib/anubis_test.go
···
632
632
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
633
633
}
634
634
}
635
+
636
+
func TestStripBasePrefixFromRequest(t *testing.T) {
637
+
testCases := []struct {
638
+
name string
639
+
basePrefix string
640
+
stripBasePrefix bool
641
+
requestPath string
642
+
expectedPath string
643
+
}{
644
+
{
645
+
name: "strip disabled - no change",
646
+
basePrefix: "/foo",
647
+
stripBasePrefix: false,
648
+
requestPath: "/foo/bar",
649
+
expectedPath: "/foo/bar",
650
+
},
651
+
{
652
+
name: "strip enabled - removes prefix",
653
+
basePrefix: "/foo",
654
+
stripBasePrefix: true,
655
+
requestPath: "/foo/bar",
656
+
expectedPath: "/bar",
657
+
},
658
+
{
659
+
name: "strip enabled - root becomes slash",
660
+
basePrefix: "/foo",
661
+
stripBasePrefix: true,
662
+
requestPath: "/foo",
663
+
expectedPath: "/",
664
+
},
665
+
{
666
+
name: "strip enabled - trailing slash on base prefix",
667
+
basePrefix: "/foo/",
668
+
stripBasePrefix: true,
669
+
requestPath: "/foo/bar",
670
+
expectedPath: "/bar",
671
+
},
672
+
{
673
+
name: "strip enabled - no prefix match",
674
+
basePrefix: "/foo",
675
+
stripBasePrefix: true,
676
+
requestPath: "/other/bar",
677
+
expectedPath: "/other/bar",
678
+
},
679
+
{
680
+
name: "strip enabled - empty base prefix",
681
+
basePrefix: "",
682
+
stripBasePrefix: true,
683
+
requestPath: "/foo/bar",
684
+
expectedPath: "/foo/bar",
685
+
},
686
+
{
687
+
name: "strip enabled - nested path",
688
+
basePrefix: "/app",
689
+
stripBasePrefix: true,
690
+
requestPath: "/app/api/v1/users",
691
+
expectedPath: "/api/v1/users",
692
+
},
693
+
{
694
+
name: "strip enabled - exact match becomes root",
695
+
basePrefix: "/myapp",
696
+
stripBasePrefix: true,
697
+
requestPath: "/myapp/",
698
+
expectedPath: "/",
699
+
},
700
+
}
701
+
702
+
for _, tc := range testCases {
703
+
t.Run(tc.name, func(t *testing.T) {
704
+
srv := &Server{
705
+
opts: Options{
706
+
BasePrefix: tc.basePrefix,
707
+
StripBasePrefix: tc.stripBasePrefix,
708
+
},
709
+
}
710
+
711
+
req := httptest.NewRequest(http.MethodGet, tc.requestPath, nil)
712
+
originalPath := req.URL.Path
713
+
714
+
result := srv.stripBasePrefixFromRequest(req)
715
+
716
+
if result.URL.Path != tc.expectedPath {
717
+
t.Errorf("expected path %q, got %q", tc.expectedPath, result.URL.Path)
718
+
}
719
+
720
+
// Ensure original request is not modified when no stripping should occur
721
+
if !tc.stripBasePrefix || tc.basePrefix == "" || !strings.HasPrefix(tc.requestPath, strings.TrimSuffix(tc.basePrefix, "/")) {
722
+
if result != req {
723
+
t.Error("expected same request object when no modification needed")
724
+
}
725
+
} else {
726
+
// Ensure original request is not modified when stripping occurs
727
+
if req.URL.Path != originalPath {
728
+
t.Error("original request was modified")
729
+
}
730
+
}
731
+
})
732
+
}
733
+
}
+1
-1
lib/challenge/error.go
+1
-1
lib/challenge/error.go
+1
lib/config.go
+1
lib/config.go
+27
lib/http.go
+27
lib/http.go
···
134
134
s.mux.ServeHTTP(w, r)
135
135
}
136
136
137
+
func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request {
138
+
if !s.opts.StripBasePrefix || s.opts.BasePrefix == "" {
139
+
return r
140
+
}
141
+
142
+
basePrefix := strings.TrimSuffix(s.opts.BasePrefix, "/")
143
+
path := r.URL.Path
144
+
145
+
if !strings.HasPrefix(path, basePrefix) {
146
+
return r
147
+
}
148
+
149
+
trimmedPath := strings.TrimPrefix(path, basePrefix)
150
+
if trimmedPath == "" {
151
+
trimmedPath = "/"
152
+
}
153
+
154
+
// Clone the request and URL
155
+
reqCopy := r.Clone(r.Context())
156
+
urlCopy := *r.URL
157
+
urlCopy.Path = trimmedPath
158
+
reqCopy.URL = &urlCopy
159
+
160
+
return reqCopy
161
+
}
162
+
137
163
func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
138
164
if s.next == nil {
139
165
redir := r.FormValue("redir")
···
158
184
).ServeHTTP(w, r)
159
185
} else {
160
186
requestsProxied.WithLabelValues(r.Host).Inc()
187
+
r = s.stripBasePrefixFromRequest(r)
161
188
s.next.ServeHTTP(w, r)
162
189
}
163
190
}