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:
parent
60ba8e9557
commit
3b3080d497
9 changed files with 155 additions and 7 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type Options struct {
|
|||
PrivateKey ed25519.PrivateKey
|
||||
CookieExpiration time.Duration
|
||||
OGTimeToLive time.Duration
|
||||
StripBasePrefix bool
|
||||
OGCacheConsidersHost bool
|
||||
OGPassthrough bool
|
||||
CookiePartitioned bool
|
||||
|
|
|
|||
27
lib/http.go
27
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue