feat: add a strip-base-prefix option (#655)

* style: fix formatting in .air.toml and installation.mdx

* feat: add --strip-base-prefix flag to modify request paths when forwarding

Closes: #638

* refactor: apply structpacking (betteralign)

* fix: add validation for strip-base-prefix and base-prefix configuration

* fix: improve request path handling by cloning request and modifying URL path

* chore: remove integration tests as they are too annoying to debug on my system
This commit is contained in:
Jason Cameron 2025-06-12 17:46:08 -04:00 committed by GitHub
parent 60ba8e9557
commit 3b3080d497
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 155 additions and 7 deletions

View file

@ -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")
}
}
})
}
}

View file

@ -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
}

View file

@ -36,6 +36,7 @@ type Options struct {
PrivateKey ed25519.PrivateKey
CookieExpiration time.Duration
OGTimeToLive time.Duration
StripBasePrefix bool
OGCacheConsidersHost bool
OGPassthrough bool
CookiePartitioned bool

View file

@ -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)
}
}

View file

@ -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 {