Procházet zdrojové kódy

support responseHeaders.set for proxy type http (#4192)

fatedier před 9 měsíci
rodič
revize
e81b36c5ba

+ 3 - 2
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
 

+ 1 - 0
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
 

+ 1 - 0
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

+ 3 - 0
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
 }
 

+ 1 - 0
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

+ 17 - 11
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)
 }
 

+ 3 - 1
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

+ 1 - 0
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,

+ 35 - 2
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)