Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build-and-push-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
3 changes: 3 additions & 0 deletions pkg/config/v1/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/msg/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
8 changes: 8 additions & 0 deletions pkg/util/vhost/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
107 changes: 107 additions & 0 deletions pkg/util/vhost/http_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
}
}
1 change: 1 addition & 0 deletions pkg/util/vhost/vhost.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ type RouteConfig struct {
Headers map[string]string
ResponseHeaders map[string]string
RouteByHTTPUser string
StripPrefix bool

CreateConnFn CreateConnFunc
ChooseEndpointFn ChooseEndpointFunc
Expand Down
1 change: 1 addition & 0 deletions server/proxy/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
51 changes: 51 additions & 0 deletions test/e2e/v1/basic/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down