From e81b36c5ba3ee0b4b88ed8832f4a302ed3289bda Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 29 Apr 2024 15:53:45 +0800 Subject: [PATCH] support responseHeaders.set for proxy type http (#4192) --- README.md | 5 +++-- Release.md | 1 + conf/frpc_full_example.toml | 1 + pkg/config/v1/proxy.go | 3 +++ pkg/msg/msg.go | 1 + pkg/util/vhost/http.go | 28 +++++++++++++++++----------- pkg/util/vhost/vhost.go | 4 +++- server/proxy/http.go | 1 + test/e2e/v1/basic/http.go | 37 +++++++++++++++++++++++++++++++++++-- 9 files changed, 65 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index aedac1f8..9122de1e 100644 --- a/README.md +++ b/README.md @@ -983,7 +983,7 @@ The HTTP request will have the `Host` header rewritten to `Host: dev.example.com ### Setting other HTTP Headers -Similar to `Host`, You can override other HTTP request headers with proxy type `http`. +Similar to `Host`, You can override other HTTP request and response headers with proxy type `http`. ```toml # frpc.toml @@ -995,9 +995,10 @@ localPort = 80 customDomains = ["test.example.com"] hostHeaderRewrite = "dev.example.com" requestHeaders.set.x-from-where = "frp" +responseHeaders.set.foo = "bar" ``` -In this example, it will set header `x-from-where: frp` in the HTTP request. +In this example, it will set header `x-from-where: frp` in the HTTP request and `foo: bar` in the HTTP response. ### Get Real IP diff --git a/Release.md b/Release.md index 12dc5488..9f3577bb 100644 --- a/Release.md +++ b/Release.md @@ -7,6 +7,7 @@ When connecting to frps versions older than v0.39.0 might encounter compatibilit ### Features * Show tcpmux proxies on the frps dashboard. +* `http` proxy can modify the response header. For example, `responseHeaders.set.foo = "bar"` will add a new header `foo: bar` to the response. ### Fixes diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 0528ddea..3d4d0347 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -209,6 +209,7 @@ locations = ["/", "/pic"] # routeByHTTPUser = abc hostHeaderRewrite = "example.com" requestHeaders.set.x-from-where = "frp" +responseHeaders.set.foo = "bar" healthCheck.type = "http" # frpc will send a GET http request '/status' to local http service # http service is alive when it return 2xx http response code diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 1949cfd3..8530e216 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -291,6 +291,7 @@ type HTTPProxyConfig struct { HTTPPassword string `json:"httpPassword,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + ResponseHeaders HeaderOperations `json:"responseHeaders,omitempty"` RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` } @@ -304,6 +305,7 @@ func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) { m.HTTPUser = c.HTTPUser m.HTTPPwd = c.HTTPPassword m.Headers = c.RequestHeaders.Set + m.ResponseHeaders = c.ResponseHeaders.Set m.RouteByHTTPUser = c.RouteByHTTPUser } @@ -317,6 +319,7 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.HTTPUser = m.HTTPUser c.HTTPPassword = m.HTTPPwd c.RequestHeaders.Set = m.Headers + c.ResponseHeaders.Set = m.ResponseHeaders c.RouteByHTTPUser = m.RouteByHTTPUser } diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index ab6d7d28..a6344d08 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -121,6 +121,7 @@ type NewProxy struct { HTTPPwd string `json:"http_pwd,omitempty"` HostHeaderRewrite string `json:"host_header_rewrite,omitempty"` Headers map[string]string `json:"headers,omitempty"` + ResponseHeaders map[string]string `json:"response_headers,omitempty"` RouteByHTTPUser string `json:"route_by_http_user,omitempty"` // stcp, sudp, xtcp diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index 518f4547..30f9631e 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -63,9 +63,9 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * req := r.Out req.URL.Scheme = "http" reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo) - oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host) + originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host) - rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) + rc := req.Context().Value(RouteConfigKey).(*RouteConfig) if rc != nil { if rc.RewriteHost != "" { req.Host = rc.RewriteHost @@ -77,7 +77,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * endpoint, _ = rc.ChooseEndpointFn() reqRouteInfo.Endpoint = endpoint log.Tracef("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]", - endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) + endpoint, originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) } // Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections. req.URL.Host = rc.Domain + "." + @@ -92,6 +92,15 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * req.URL.Host = req.Host } }, + ModifyResponse: func(r *http.Response) error { + rc := r.Request.Context().Value(RouteConfigKey).(*RouteConfig) + if rc != nil { + for k, v := range rc.ResponseHeaders { + r.Header.Set(k, v) + } + } + return nil + }, // Create a connection to one proxy routed by route policy. Transport: &http.Transport{ ResponseHeaderTimeout: rp.responseHeaderTimeout, @@ -157,14 +166,6 @@ func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser str return nil } -func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) { - vr, ok := rp.getVhost(domain, location, routeByHTTPUser) - if ok { - headers = vr.payload.(*RouteConfig).Headers - } - return -} - // CreateConnection create a new connection by route config func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) { host, _ := httppkg.CanonicalHost(reqRouteInfo.Host) @@ -305,8 +306,13 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ RemoteAddr: req.RemoteAddr, URLHost: req.URL.Host, } + + originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host) + rc := rp.GetRouteConfig(originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) + newctx := req.Context() newctx = context.WithValue(newctx, RouteInfoKey, reqRouteInfo) + newctx = context.WithValue(newctx, RouteConfigKey, rc) return req.Clone(newctx) } diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index b1370599..e62a1caf 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -29,7 +29,8 @@ import ( type RouteInfo string const ( - RouteInfoKey RouteInfo = "routeInfo" + RouteInfoKey RouteInfo = "routeInfo" + RouteConfigKey RouteInfo = "routeConfig" ) type RequestRouteInfo struct { @@ -113,6 +114,7 @@ type RouteConfig struct { Username string Password string Headers map[string]string + ResponseHeaders map[string]string RouteByHTTPUser string CreateConnFn CreateConnFunc diff --git a/server/proxy/http.go b/server/proxy/http.go index cd4c4b96..9a02dcdd 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -58,6 +58,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { RewriteHost: pxy.cfg.HostHeaderRewrite, RouteByHTTPUser: pxy.cfg.RouteByHTTPUser, Headers: pxy.cfg.RequestHeaders.Set, + ResponseHeaders: pxy.cfg.ResponseHeaders.Set, Username: pxy.cfg.HTTPUser, Password: pxy.cfg.HTTPPassword, CreateConnFn: pxy.GetRealConn, diff --git a/test/e2e/v1/basic/http.go b/test/e2e/v1/basic/http.go index 4883263e..c37e84e7 100644 --- a/test/e2e/v1/basic/http.go +++ b/test/e2e/v1/basic/http.go @@ -267,7 +267,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { Ensure() }) - ginkgo.It("Modify headers", func() { + ginkgo.It("Modify request headers", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) @@ -292,7 +292,6 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { f.RunProcesses([]string{serverConf}, []string{clientConf}) - // not set auth header framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { r.HTTP().HTTPHost("normal.example.com") @@ -301,6 +300,40 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { Ensure() }) + ginkgo.It("Modify response headers", 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.WriteHeader(200) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + responseHeaders.set.x-from-where = "frp" + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + Ensure(func(res *request.Response) bool { + return res.Header.Get("X-From-Where") == "frp" + }) + }) + ginkgo.It("Host Header Rewrite", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort)