Compare commits

...

2 Commits

Author SHA1 Message Date
Xe Iaso
d6f02ac5f9 fix(lib): try to make base path prefixing work again
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-12 19:07:19 +00:00
Xe Iaso
2ea8296682 feat(lib): enable multiple consecutive slash support
Replaces #808
Closes #754

Some web applications require the ability to include multiple
consecutive slashes in a URL. This could be for optional path variables
or for wiki article titles that start with a leading slash.

I wasn't aware that the RFC allowed this.

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-12 18:00:03 +00:00
5 changed files with 81 additions and 17 deletions

View File

@@ -99,6 +99,7 @@ There are a bunch of other assorted features and fixes too:
- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
- The [bbolt storage backend](./admin/policies.mdx#bbolt) now runs its cleanup every hour instead of every five minutes.
- Don't block Anubis starting up if [Thoth](./admin/thoth.mdx) health checks fail.
- Multiple consecutive slashes are supported in upstream application URLs ([#754](https://github.com/TecharoHQ/anubis/issues/754)).
### Potentially breaking changes

View File

@@ -67,14 +67,15 @@ var (
)
type Server struct {
next http.Handler
mux *http.ServeMux
policy *policy.ParsedConfig
OGTags *ogtags.OGTagCache
ed25519Priv ed25519.PrivateKey
hs512Secret []byte
opts Options
store store.Interface
next http.Handler
mux *http.ServeMux
policy *policy.ParsedConfig
OGTags *ogtags.OGTagCache
ed25519Priv ed25519.PrivateKey
hs512Secret []byte
opts Options
store store.Interface
internalPath string
}
func (s *Server) getTokenKeyfunc() jwt.Keyfunc {

View File

@@ -204,6 +204,63 @@ func TestCVE2025_24369(t *testing.T) {
}
}
func TestDoubleSlashes(t *testing.T) {
pol := loadPolicies(t, "", 0)
path := ""
srv := spawnAnubis(t, Options{
Next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path = r.URL.Path
}),
Policy: pol,
})
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
defer ts.Close()
cli := httpClient(t)
chall := makeChallenge(t, ts, cli)
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
if resp.StatusCode != http.StatusFound {
t.Fatal("can't solve challenge, see logs")
}
for _, tt := range []struct {
name, path string
}{
{
name: "basic",
path: "/foo",
},
{
name: "leading slashes",
path: "//foo",
},
{
name: "mid slashes",
path: "/foo//bar///baz",
},
{
name: "trailing slashes",
path: "/foo/bar///",
},
} {
t.Run(tt.name, func(t *testing.T) {
if _, err := cli.Get(ts.URL + tt.path); err != nil {
t.Errorf("can't make request to %s: %v", tt.path, err)
}
if path != tt.path {
t.Logf("want: %s", tt.path)
t.Logf("got: %s", path)
t.Error("invalid path sent to server")
}
})
}
}
func TestCookieCustomExpiration(t *testing.T) {
pol := loadPolicies(t, "", 0)
ckieExpiration := 10 * time.Minute

View File

@@ -101,13 +101,14 @@ func New(opts Options) (*Server, error) {
anubis.BasePrefix = opts.BasePrefix
result := &Server{
next: opts.Next,
ed25519Priv: opts.ED25519PrivateKey,
hs512Secret: opts.HS512Secret,
policy: opts.Policy,
opts: opts,
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store),
store: opts.Policy.Store,
next: opts.Next,
ed25519Priv: opts.ED25519PrivateKey,
hs512Secret: opts.HS512Secret,
policy: opts.Policy,
opts: opts,
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store),
store: opts.Policy.Store,
internalPath: opts.BasePrefix + anubis.StaticPath,
}
mux := http.NewServeMux()
@@ -154,7 +155,6 @@ func New(opts Options) (*Server, error) {
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
//goland:noinspection GoBoolExpressions
if anubis.Version == "devel" {

View File

@@ -200,7 +200,12 @@ func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg s
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
switch strings.HasPrefix(r.URL.Path, s.internalPath) {
case true:
s.mux.ServeHTTP(w, r)
case false:
s.maybeReverseProxyOrPage(w, r)
}
}
func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request {