From 521542b796331203bdd24d83829c5f91fafe6d7d Mon Sep 17 00:00:00 2001 From: Mxmilu666 Date: Sat, 4 Jul 2026 14:30:52 +0800 Subject: [PATCH] feat(proxy): add HTTP to HTTPS redirection support --- conf/frpc_full_example.toml | 4 ++ conf/frps_full_example.toml | 12 ++++ go.mod | 2 - go.sum | 4 +- pkg/config/v1/proxy.go | 8 +++ pkg/config/v1/server.go | 13 +++++ pkg/msg/msg.go | 1 + pkg/util/vhost/http.go | 15 ++++- pkg/util/vhost/https_redirect.go | 83 +++++++++++++++++++++++++++ pkg/util/vhost/https_redirect_test.go | 46 +++++++++++++++ server/controller/resource.go | 3 + server/proxy/https.go | 19 ++++++ server/proxy/idle_watcher.go | 47 +++++++++++++++ server/proxy/idle_watcher_test.go | 52 +++++++++++++++++ server/proxy/proxy.go | 18 ++++++ server/service.go | 6 ++ 16 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 pkg/util/vhost/https_redirect.go create mode 100644 pkg/util/vhost/https_redirect_test.go create mode 100644 server/proxy/idle_watcher.go create mode 100644 server/proxy/idle_watcher_test.go diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index e4e15605..67b4c7b7 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -275,6 +275,10 @@ localIP = "127.0.0.1" localPort = 8000 subdomain = "web02" customDomains = ["web02.yourdomain.com"] +# if true, frps will redirect plain HTTP requests for the domains above to HTTPS. +# It requires vhostHTTPPort to be enabled on frps, and an HTTP proxy registered +# on the same domain always takes precedence over the redirect. +# httpRedirect = true # if not empty, frpc will use proxy protocol to transfer connection info to your local service # v1 or v2 or empty transport.proxyProtocolVersion = "v2" diff --git a/conf/frps_full_example.toml b/conf/frps_full_example.toml index aba37435..08f395c7 100644 --- a/conf/frps_full_example.toml +++ b/conf/frps_full_example.toml @@ -40,6 +40,12 @@ transport.maxPoolCount = 5 # If negative, keep-alive probes are disabled. # transport.tcpKeepalive = 7200 +# proxyIdleTimeout specifies the maximum time in seconds that a proxied user connection +# can stay open without any traffic in either direction before frps closes it. +# It reclaims connections whose endpoints are still open at the TCP level but will never send data again. +# By default, this value is 0, which disables the idle timeout. +# transport.proxyIdleTimeout = 7200 + # transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false. transport.tls.force = false @@ -52,6 +58,12 @@ transport.tls.force = false vhostHTTPPort = 80 vhostHTTPSPort = 443 +# Port used in the Location header when redirecting HTTP requests to HTTPS for +# proxies with httpRedirect enabled. Set it when browsers reach the HTTPS vhost +# through a port mapping, so it differs from vhostHTTPSPort. +# By default, this value is vhostHTTPSPort. +# vhostHTTPSRedirectPort = 443 + # Response header timeout(seconds) for vhost http server, default is 60s # vhostHTTPTimeout = 60 diff --git a/go.mod b/go.mod index 0f5ce96c..0db215eb 100644 --- a/go.mod +++ b/go.mod @@ -97,5 +97,3 @@ require ( // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 - -replace github.com/fatedier/golib => github.com/Lolia-FRP/golib v0.7.4 diff --git a/go.sum b/go.sum index af61416c..ddb9e32a 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Lolia-FRP/golib v0.7.4 h1:EtUJChdgXVV9iU1xqB7PkD/1/3RgATbj3n86HbrLKMA= -github.com/Lolia-FRP/golib v0.7.4/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -36,6 +34,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatedier/golib v0.7.0 h1:tMDF9ObcwVt59VUHroJOzHQjVFPLymZVMpGm9WAVwhY= +github.com/fatedier/golib v0.7.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index e7d1a6b7..4c156702 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -369,6 +369,12 @@ var _ ProxyConfigurer = &HTTPSProxyConfig{} type HTTPSProxyConfig struct { ProxyBaseConfig DomainConfig + + // HTTPRedirect requests frps to redirect plain HTTP requests for the + // proxy's domains to their HTTPS endpoint. It only takes effect when + // frps has vhostHTTPPort enabled, and never overrides a real HTTP + // proxy registered on the same domain. + HTTPRedirect bool `json:"httpRedirect,omitempty"` } func (c *HTTPSProxyConfig) MarshalToMsg(m *msg.NewProxy) { @@ -376,6 +382,7 @@ func (c *HTTPSProxyConfig) MarshalToMsg(m *msg.NewProxy) { m.CustomDomains = c.CustomDomains m.SubDomain = c.SubDomain + m.HTTPRedirect = c.HTTPRedirect } func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { @@ -383,6 +390,7 @@ func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { c.CustomDomains = m.CustomDomains c.SubDomain = m.SubDomain + c.HTTPRedirect = m.HTTPRedirect } func (c *HTTPSProxyConfig) Clone() ProxyConfigurer { diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index a92aac97..b0b2baa4 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -51,6 +51,12 @@ type ServerConfig struct { // Vhost requests. If this value is 0, the server will not listen for HTTPS // requests. VhostHTTPSPort int `json:"vhostHTTPSPort,omitempty"` + // VhostHTTPSRedirectPort specifies the port used in the Location header + // when redirecting HTTP requests to HTTPS for proxies with httpRedirect + // enabled. Set it when browsers reach the HTTPS vhost through a port + // mapping, so it differs from VhostHTTPSPort. By default, this value is + // VhostHTTPSPort. + VhostHTTPSRedirectPort int `json:"vhostHTTPSRedirectPort,omitempty"` // TCPMuxHTTPConnectPort specifies the port that the server listens for TCP // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP // requests on one single port. If it's not - it will listen on this value for @@ -118,6 +124,7 @@ func (c *ServerConfig) Complete() error { } c.VhostHTTPTimeout = util.EmptyOr(c.VhostHTTPTimeout, 60) + c.VhostHTTPSRedirectPort = util.EmptyOr(c.VhostHTTPSRedirectPort, c.VhostHTTPSPort) c.DetailedErrorsToClient = util.EmptyOr(c.DetailedErrorsToClient, lo.ToPtr(true)) c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10) c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) @@ -173,6 +180,12 @@ type ServerTransportConfig struct { // before terminating the connection. It is not recommended to change this // value. By default, this value is 90. Set negative value to disable it. HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"` + // ProxyIdleTimeout specifies the maximum time in seconds that a proxied + // user connection can stay open without any traffic in either direction + // before frps closes it. This reclaims connections whose endpoints are + // still open at the TCP level but will never send data again. By default, + // this value is 0, which disables the idle timeout. + ProxyIdleTimeout int64 `json:"proxyIdleTimeout,omitempty"` // QUIC options. QUIC QUICOptions `json:"quic,omitempty"` // TLS specifies TLS settings for the connection from the client. diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index 52948bd5..67e1756d 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"` + HTTPRedirect bool `json:"http_redirect,omitempty"` // stcp, sudp, xtcp Sk string `json:"sk,omitempty"` diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index 865ff9d5..725662d8 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -39,11 +39,15 @@ var ErrNoRouteFound = errors.New("no route found") type HTTPReverseProxyOptions struct { ResponseHeaderTimeoutS int64 + // HTTPSRedirector, if set, redirects requests whose host has no HTTP + // route but opted into HTTP to HTTPS redirection. + HTTPSRedirector *HTTPSRedirector } type HTTPReverseProxy struct { - proxy http.Handler - vhostRouter *Routers + proxy http.Handler + vhostRouter *Routers + httpsRedirector *HTTPSRedirector responseHeaderTimeout time.Duration } @@ -55,6 +59,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * rp := &HTTPReverseProxy{ responseHeaderTimeout: time.Duration(option.ResponseHeaderTimeoutS) * time.Second, vhostRouter: vhostRouter, + httpsRedirector: option.HTTPSRedirector, } proxy := &httputil.ReverseProxy{ // Modify incoming requests by route policies. @@ -281,6 +286,12 @@ func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) return } + // Hosts without any real HTTP route may still ask for a redirect to + // their HTTPS endpoint; registered HTTP routes always take precedence. + if rc == nil && rp.httpsRedirector != nil && rp.httpsRedirector.Redirect(rw, newreq) { + return + } + if req.Method == http.MethodConnect { rp.connectHandler(rw, newreq) } else { diff --git a/pkg/util/vhost/https_redirect.go b/pkg/util/vhost/https_redirect.go new file mode 100644 index 00000000..c1acbf27 --- /dev/null +++ b/pkg/util/vhost/https_redirect.go @@ -0,0 +1,83 @@ +// Copyright 2026 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vhost + +import ( + "net/http" + "strconv" + "sync" + + httppkg "github.com/fatedier/frp/pkg/util/http" +) + +// HTTPSRedirector tracks domains of HTTPS proxies that opted into automatic +// HTTP to HTTPS redirection. The HTTP reverse proxy consults it as a fallback +// for hosts that have no real HTTP route, so registered HTTP proxies on the +// same domain always take precedence over the redirect. +type HTTPSRedirector struct { + mu sync.RWMutex + domains map[string]int // domain -> number of proxies requesting the redirect + port int // port used in the redirect Location, as seen by browsers +} + +func NewHTTPSRedirector(port int) *HTTPSRedirector { + return &HTTPSRedirector{ + domains: make(map[string]int), + port: port, + } +} + +func (r *HTTPSRedirector) Add(domain string) { + r.mu.Lock() + defer r.mu.Unlock() + r.domains[domain]++ +} + +func (r *HTTPSRedirector) Remove(domain string) { + r.mu.Lock() + defer r.mu.Unlock() + if r.domains[domain] <= 1 { + delete(r.domains, domain) + } else { + r.domains[domain]-- + } +} + +func (r *HTTPSRedirector) match(host string) bool { + r.mu.RLock() + defer r.mu.RUnlock() + return r.domains[host] > 0 +} + +// Redirect responds with a redirect to the HTTPS endpoint of the requested +// host if the host opted into redirection. It reports whether the request +// was handled. +func (r *HTTPSRedirector) Redirect(rw http.ResponseWriter, req *http.Request) bool { + if req.Method == http.MethodConnect { + return false + } + host, err := httppkg.CanonicalHost(req.Host) + if err != nil || !r.match(host) { + return false + } + + target := "https://" + host + if r.port != 443 { + target += ":" + strconv.Itoa(r.port) + } + target += req.URL.RequestURI() + http.Redirect(rw, req, target, http.StatusMovedPermanently) + return true +} diff --git a/pkg/util/vhost/https_redirect_test.go b/pkg/util/vhost/https_redirect_test.go new file mode 100644 index 00000000..39b03c1b --- /dev/null +++ b/pkg/util/vhost/https_redirect_test.go @@ -0,0 +1,46 @@ +package vhost + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHTTPSRedirectorRedirect(t *testing.T) { + r := NewHTTPSRedirector(443) + r.Add("a.example.com") + + // registered domain, port stripped from host, path and query preserved + req := httptest.NewRequest("GET", "http://a.example.com:8080/foo?x=1", nil) + rw := httptest.NewRecorder() + require.True(t, r.Redirect(rw, req)) + require.Equal(t, 301, rw.Code) + require.Equal(t, "https://a.example.com/foo?x=1", rw.Header().Get("Location")) + + // unknown domain is not handled + req = httptest.NewRequest("GET", "http://b.example.com/", nil) + require.False(t, r.Redirect(httptest.NewRecorder(), req)) +} + +func TestHTTPSRedirectorNonDefaultPort(t *testing.T) { + r := NewHTTPSRedirector(8443) + r.Add("a.example.com") + + req := httptest.NewRequest("GET", "http://a.example.com/", nil) + rw := httptest.NewRecorder() + require.True(t, r.Redirect(rw, req)) + require.Equal(t, "https://a.example.com:8443/", rw.Header().Get("Location")) +} + +func TestHTTPSRedirectorRefCount(t *testing.T) { + r := NewHTTPSRedirector(443) + r.Add("a.example.com") + r.Add("a.example.com") + + r.Remove("a.example.com") + require.True(t, r.match("a.example.com"), "domain removed while another proxy still references it") + + r.Remove("a.example.com") + require.False(t, r.match("a.example.com")) +} diff --git a/server/controller/resource.go b/server/controller/resource.go index 717c9616..05ff6310 100644 --- a/server/controller/resource.go +++ b/server/controller/resource.go @@ -50,6 +50,9 @@ type ResourceController struct { // For HTTP proxies, forwarding HTTP requests HTTPReverseProxy *vhost.HTTPReverseProxy + // Domains of HTTPS proxies that requested automatic HTTP to HTTPS redirection + HTTPSRedirector *vhost.HTTPSRedirector + // For HTTPS proxies, route requests to different clients by hostname and other information VhostHTTPSMuxer *vhost.HTTPSMuxer diff --git a/server/proxy/https.go b/server/proxy/https.go index e2efc736..5744921e 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -31,6 +31,9 @@ func init() { type HTTPSProxy struct { *BaseProxy cfg *v1.HTTPSProxyConfig + + // domains registered for HTTP to HTTPS redirection, removed on Close + redirectDomains []string } func NewHTTPSProxy(baseProxy *BaseProxy) Proxy { @@ -66,12 +69,28 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group) } + if pxy.cfg.HTTPRedirect { + if pxy.rc.HTTPSRedirector != nil { + for _, domain := range domains { + pxy.rc.HTTPSRedirector.Add(domain) + } + pxy.redirectDomains = domains + xl.Infof("https proxy enabled http redirect for hosts %v", domains) + } else { + xl.Warnf("httpRedirect is enabled but frps has no vhostHTTPPort or vhostHTTPSPort, ignored") + } + } + pxy.startCommonTCPListenersHandler() remoteAddr = strings.Join(addrs, ",") return } func (pxy *HTTPSProxy) Close() { + for _, domain := range pxy.redirectDomains { + pxy.rc.HTTPSRedirector.Remove(domain) + } + pxy.redirectDomains = nil pxy.BaseProxy.Close() } diff --git a/server/proxy/idle_watcher.go b/server/proxy/idle_watcher.go new file mode 100644 index 00000000..d853040d --- /dev/null +++ b/server/proxy/idle_watcher.go @@ -0,0 +1,47 @@ +package proxy + +import ( + "sync/atomic" + "time" +) + +// idleWatcher closes a proxied connection pair when no bytes have flowed in +// either direction for the configured timeout. Without it, endpoints that +// stay open at the TCP level but never send data again would pin the join +// goroutines and their transfer buffers forever. +type idleWatcher struct { + lastActive atomic.Int64 // unix nano of the last byte transferred + stopCh chan struct{} +} + +func startIdleWatcher(timeout time.Duration, closeFn func()) *idleWatcher { + w := &idleWatcher{stopCh: make(chan struct{})} + w.touch() + + go func() { + timer := time.NewTimer(timeout) + defer timer.Stop() + for { + select { + case <-w.stopCh: + return + case <-timer.C: + idle := time.Since(time.Unix(0, w.lastActive.Load())) + if idle >= timeout { + closeFn() + return + } + timer.Reset(timeout - idle) + } + } + }() + return w +} + +func (w *idleWatcher) touch() { + w.lastActive.Store(time.Now().UnixNano()) +} + +func (w *idleWatcher) stop() { + close(w.stopCh) +} diff --git a/server/proxy/idle_watcher_test.go b/server/proxy/idle_watcher_test.go new file mode 100644 index 00000000..2e75e1d3 --- /dev/null +++ b/server/proxy/idle_watcher_test.go @@ -0,0 +1,52 @@ +package proxy + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestIdleWatcherClosesAfterTimeout(t *testing.T) { + closed := make(chan struct{}) + w := startIdleWatcher(50*time.Millisecond, func() { + close(closed) + }) + defer w.stop() + + select { + case <-closed: + case <-time.After(time.Second): + t.Fatal("closeFn was not called after idle timeout") + } +} + +func TestIdleWatcherTouchPostponesClose(t *testing.T) { + var closedFlag atomic.Bool + w := startIdleWatcher(100*time.Millisecond, func() { + closedFlag.Store(true) + }) + defer w.stop() + + // keep the connection "active" well past the timeout + for range 6 { + time.Sleep(30 * time.Millisecond) + w.touch() + } + require.False(t, closedFlag.Load(), "closeFn fired despite ongoing activity") + + // once activity stops, it should still close + require.Eventually(t, closedFlag.Load, time.Second, 10*time.Millisecond) +} + +func TestIdleWatcherStopPreventsClose(t *testing.T) { + var closedFlag atomic.Bool + w := startIdleWatcher(50*time.Millisecond, func() { + closedFlag.Store(true) + }) + w.stop() + + time.Sleep(150 * time.Millisecond) + require.False(t, closedFlag.Load(), "closeFn fired after watcher was stopped") +} diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index 4e6ceb7a..002b5f8a 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -317,15 +317,33 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { name := pxy.GetName() proxyType := cfg.Type + var watcher *idleWatcher + touch := func() { + if watcher != nil { + watcher.touch() + } + } local = wrapCountingReadWriteCloser(local, nil, func(bytes int64) { + touch() metrics.Server.AddTrafficIn(name, proxyType, bytes) }) userConn = netpkg.WrapReadWriteCloserToConn( wrapCountingReadWriteCloser(userConn, nil, func(bytes int64) { + touch() metrics.Server.AddTrafficOut(name, proxyType, bytes) }), userConn, ) + if idleTimeout := time.Duration(pxy.serverCfg.Transport.ProxyIdleTimeout) * time.Second; idleTimeout > 0 { + wrappedLocal, wrappedUserConn := local, userConn + watcher = startIdleWatcher(idleTimeout, func() { + xl.Infof("close user connection [%s] of proxy [%s]: no traffic in either direction for %s", + wrappedUserConn.RemoteAddr().String(), name, idleTimeout) + _ = wrappedUserConn.Close() + _ = wrappedLocal.Close() + }) + defer watcher.stop() + } metrics.Server.OpenConnection(name, proxyType) // Traffic is counted incrementally via the counting wrappers above, so the // byte totals returned by joinUserConnection are intentionally discarded here. diff --git a/server/service.go b/server/service.go index f3f9e323..05f24a87 100644 --- a/server/service.go +++ b/server/service.go @@ -291,8 +291,14 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { // Create http vhost muxer. if cfg.VhostHTTPPort > 0 { + var redirector *vhost.HTTPSRedirector + if cfg.VhostHTTPSPort > 0 { + redirector = vhost.NewHTTPSRedirector(cfg.VhostHTTPSRedirectPort) + svr.rc.HTTPSRedirector = redirector + } rp := vhost.NewHTTPReverseProxy(vhost.HTTPReverseProxyOptions{ ResponseHeaderTimeoutS: cfg.VhostHTTPTimeout, + HTTPSRedirector: redirector, }, svr.httpVhostRouter) svr.rc.HTTPReverseProxy = rp