diff --git a/.github/workflows/build-and-push-image.yml b/.github/workflows/build-and-push-image.yml index 425f7e39ef..8cbddfff0b 100644 --- a/.github/workflows/build-and-push-image.yml +++ b/.github/workflows/build-and-push-image.yml @@ -78,6 +78,8 @@ jobs: file: ./dockerfiles/Dockerfile-for-frps platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x push: true + provenance: false + sbom: false tags: | ${{ env.TAG_FRPS }} ${{ env.TAG_FRPS_GPR }} diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index e7d1a6b79b..2d4ff8175a 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -324,6 +324,7 @@ type HTTPProxyConfig struct { RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` ResponseHeaders HeaderOperations `json:"responseHeaders,omitempty"` RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` + StripPrefix bool `json:"stripPrefix,omitempty"` } func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) { @@ -338,6 +339,7 @@ func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) { m.Headers = c.RequestHeaders.Set m.ResponseHeaders = c.ResponseHeaders.Set m.RouteByHTTPUser = c.RouteByHTTPUser + m.StripPrefix = c.StripPrefix } func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { @@ -352,6 +354,7 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.RequestHeaders.Set = m.Headers c.ResponseHeaders.Set = m.ResponseHeaders c.RouteByHTTPUser = m.RouteByHTTPUser + c.StripPrefix = m.StripPrefix } func (c *HTTPProxyConfig) Clone() ProxyConfigurer { diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index e8bcbc359f..c08feea6a7 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -124,6 +124,7 @@ type NewProxy struct { Headers map[string]string `json:"headers,omitempty"` ResponseHeaders map[string]string `json:"response_headers,omitempty"` RouteByHTTPUser string `json:"route_by_http_user,omitempty"` + StripPrefix bool `json:"strip_prefix,omitempty"` // stcp, sudp, xtcp Sk string `json:"sk,omitempty"` diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index d12e791661..c0ae017aad 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -73,6 +73,14 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * req.Host = rc.RewriteHost } + // Strip prefix if enabled and location matches + if rc.StripPrefix && rc.Location != "" && strings.HasPrefix(req.URL.Path, rc.Location) { + req.URL.Path = strings.TrimPrefix(req.URL.Path, rc.Location) + if req.URL.Path == "" { + req.URL.Path = "/" + } + } + var endpoint string if rc.ChooseEndpointFn != nil { // ignore error here, it will use CreateConnFn instead later diff --git a/pkg/util/vhost/http_test.go b/pkg/util/vhost/http_test.go new file mode 100644 index 0000000000..b4998e920a --- /dev/null +++ b/pkg/util/vhost/http_test.go @@ -0,0 +1,107 @@ +package vhost + +import ( + "net/http" + "net/http/httptest" + "net/http/httputil" + "strings" + "testing" +) + +func TestStripPrefix(t *testing.T) { + tests := []struct { + name string + location string + stripPrefix bool + requestPath string + expectedPath string + }{ + { + name: "strip prefix enabled with matching location", + location: "/api", + stripPrefix: true, + requestPath: "/api/users", + expectedPath: "/users", + }, + { + name: "strip prefix enabled with exact match", + location: "/api", + stripPrefix: true, + requestPath: "/api", + expectedPath: "/", + }, + { + name: "strip prefix enabled with nested path", + location: "/api", + stripPrefix: true, + requestPath: "/api/v1/data", + expectedPath: "/v1/data", + }, + { + name: "strip prefix disabled", + location: "/api", + stripPrefix: false, + requestPath: "/api/users", + expectedPath: "/api/users", + }, + { + name: "strip prefix enabled but path doesn't match", + location: "/api", + stripPrefix: true, + requestPath: "/other/path", + expectedPath: "/other/path", + }, + { + name: "empty location", + location: "", + stripPrefix: true, + requestPath: "/api/users", + expectedPath: "/api/users", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a test server that echoes the request path + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(r.URL.Path)) + })) + defer backend.Close() + + // Create the reverse proxy with our rewrite logic + proxy := &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + req := r.Out + req.URL.Scheme = "http" + req.URL.Host = strings.TrimPrefix(backend.URL, "http://") + + // Simulate the RouteConfig being set in context + rc := &RouteConfig{ + Location: tt.location, + StripPrefix: tt.stripPrefix, + } + + // Apply the strip prefix logic + if rc.StripPrefix && rc.Location != "" && strings.HasPrefix(req.URL.Path, rc.Location) { + req.URL.Path = strings.TrimPrefix(req.URL.Path, rc.Location) + if req.URL.Path == "" { + req.URL.Path = "/" + } + } + }, + } + + // Create a test request + req := httptest.NewRequest("GET", "http://example.com"+tt.requestPath, nil) + w := httptest.NewRecorder() + + // Execute the proxy + proxy.ServeHTTP(w, req) + + // Check the result + if w.Body.String() != tt.expectedPath { + t.Errorf("Expected path %q, got %q", tt.expectedPath, w.Body.String()) + } + }) + } +} diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 007751d7f3..ef4481d0f6 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -120,6 +120,7 @@ type RouteConfig struct { Headers map[string]string ResponseHeaders map[string]string RouteByHTTPUser string + StripPrefix bool CreateConnFn CreateConnFunc ChooseEndpointFn ChooseEndpointFunc diff --git a/server/proxy/http.go b/server/proxy/http.go index 05afc2e982..9f08e7a373 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -61,6 +61,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { ResponseHeaders: pxy.cfg.ResponseHeaders.Set, Username: pxy.cfg.HTTPUser, Password: pxy.cfg.HTTPPassword, + StripPrefix: pxy.cfg.StripPrefix, CreateConnFn: pxy.GetRealConn, } diff --git a/test/e2e/v1/basic/http.go b/test/e2e/v1/basic/http.go index b594f1ea81..f08ac6b618 100644 --- a/test/e2e/v1/basic/http.go +++ b/test/e2e/v1/basic/http.go @@ -420,6 +420,57 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { framework.ExpectEqualValues(consts.TestString, string(msg)) }) + ginkgo.It("Strip prefix from request path", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.URL.Path)) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + locations = ["/api"] + stripPrefix = true + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // Test that /api/users becomes /users + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPPath("/api/users") + }). + ExpectResp([]byte("/users")). + Ensure() + + // Test that /api becomes / + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPPath("/api") + }). + ExpectResp([]byte("/")). + Ensure() + + // Test that /api/v1/data becomes /v1/data + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPPath("/api/v1/data") + }). + ExpectResp([]byte("/v1/data")). + Ensure() + }) + ginkgo.It("vhostHTTPTimeout", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort)