forked from Mxmilu666/frp
feat(proxy): add HTTP to HTTPS redirection support
This commit is contained in:
@@ -275,6 +275,10 @@ localIP = "127.0.0.1"
|
|||||||
localPort = 8000
|
localPort = 8000
|
||||||
subdomain = "web02"
|
subdomain = "web02"
|
||||||
customDomains = ["web02.yourdomain.com"]
|
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
|
# if not empty, frpc will use proxy protocol to transfer connection info to your local service
|
||||||
# v1 or v2 or empty
|
# v1 or v2 or empty
|
||||||
transport.proxyProtocolVersion = "v2"
|
transport.proxyProtocolVersion = "v2"
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ transport.maxPoolCount = 5
|
|||||||
# If negative, keep-alive probes are disabled.
|
# If negative, keep-alive probes are disabled.
|
||||||
# transport.tcpKeepalive = 7200
|
# 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 specifies whether to only accept TLS-encrypted connections. By default, the value is false.
|
||||||
transport.tls.force = false
|
transport.tls.force = false
|
||||||
|
|
||||||
@@ -52,6 +58,12 @@ transport.tls.force = false
|
|||||||
vhostHTTPPort = 80
|
vhostHTTPPort = 80
|
||||||
vhostHTTPSPort = 443
|
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
|
# Response header timeout(seconds) for vhost http server, default is 60s
|
||||||
# vhostHTTPTimeout = 60
|
# vhostHTTPTimeout = 60
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
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.
|
// 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/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6
|
||||||
|
|
||||||
replace github.com/fatedier/golib => github.com/Lolia-FRP/golib v0.7.4
|
|
||||||
|
|||||||
4
go.sum
4
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 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
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/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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
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=
|
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.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
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/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 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
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=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
|
|||||||
@@ -369,6 +369,12 @@ var _ ProxyConfigurer = &HTTPSProxyConfig{}
|
|||||||
type HTTPSProxyConfig struct {
|
type HTTPSProxyConfig struct {
|
||||||
ProxyBaseConfig
|
ProxyBaseConfig
|
||||||
DomainConfig
|
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) {
|
func (c *HTTPSProxyConfig) MarshalToMsg(m *msg.NewProxy) {
|
||||||
@@ -376,6 +382,7 @@ func (c *HTTPSProxyConfig) MarshalToMsg(m *msg.NewProxy) {
|
|||||||
|
|
||||||
m.CustomDomains = c.CustomDomains
|
m.CustomDomains = c.CustomDomains
|
||||||
m.SubDomain = c.SubDomain
|
m.SubDomain = c.SubDomain
|
||||||
|
m.HTTPRedirect = c.HTTPRedirect
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
||||||
@@ -383,6 +390,7 @@ func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
|
|
||||||
c.CustomDomains = m.CustomDomains
|
c.CustomDomains = m.CustomDomains
|
||||||
c.SubDomain = m.SubDomain
|
c.SubDomain = m.SubDomain
|
||||||
|
c.HTTPRedirect = m.HTTPRedirect
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HTTPSProxyConfig) Clone() ProxyConfigurer {
|
func (c *HTTPSProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ type ServerConfig struct {
|
|||||||
// Vhost requests. If this value is 0, the server will not listen for HTTPS
|
// Vhost requests. If this value is 0, the server will not listen for HTTPS
|
||||||
// requests.
|
// requests.
|
||||||
VhostHTTPSPort int `json:"vhostHTTPSPort,omitempty"`
|
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
|
// TCPMuxHTTPConnectPort specifies the port that the server listens for TCP
|
||||||
// HTTP CONNECT requests. If the value is 0, the server will not multiplex 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
|
// 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.VhostHTTPTimeout = util.EmptyOr(c.VhostHTTPTimeout, 60)
|
||||||
|
c.VhostHTTPSRedirectPort = util.EmptyOr(c.VhostHTTPSRedirectPort, c.VhostHTTPSPort)
|
||||||
c.DetailedErrorsToClient = util.EmptyOr(c.DetailedErrorsToClient, lo.ToPtr(true))
|
c.DetailedErrorsToClient = util.EmptyOr(c.DetailedErrorsToClient, lo.ToPtr(true))
|
||||||
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
|
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
|
||||||
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
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
|
// before terminating the connection. It is not recommended to change this
|
||||||
// value. By default, this value is 90. Set negative value to disable it.
|
// value. By default, this value is 90. Set negative value to disable it.
|
||||||
HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"`
|
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 options.
|
||||||
QUIC QUICOptions `json:"quic,omitempty"`
|
QUIC QUICOptions `json:"quic,omitempty"`
|
||||||
// TLS specifies TLS settings for the connection from the client.
|
// TLS specifies TLS settings for the connection from the client.
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ type NewProxy struct {
|
|||||||
Headers map[string]string `json:"headers,omitempty"`
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
ResponseHeaders map[string]string `json:"response_headers,omitempty"`
|
ResponseHeaders map[string]string `json:"response_headers,omitempty"`
|
||||||
RouteByHTTPUser string `json:"route_by_http_user,omitempty"`
|
RouteByHTTPUser string `json:"route_by_http_user,omitempty"`
|
||||||
|
HTTPRedirect bool `json:"http_redirect,omitempty"`
|
||||||
|
|
||||||
// stcp, sudp, xtcp
|
// stcp, sudp, xtcp
|
||||||
Sk string `json:"sk,omitempty"`
|
Sk string `json:"sk,omitempty"`
|
||||||
|
|||||||
@@ -39,11 +39,15 @@ var ErrNoRouteFound = errors.New("no route found")
|
|||||||
|
|
||||||
type HTTPReverseProxyOptions struct {
|
type HTTPReverseProxyOptions struct {
|
||||||
ResponseHeaderTimeoutS int64
|
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 {
|
type HTTPReverseProxy struct {
|
||||||
proxy http.Handler
|
proxy http.Handler
|
||||||
vhostRouter *Routers
|
vhostRouter *Routers
|
||||||
|
httpsRedirector *HTTPSRedirector
|
||||||
|
|
||||||
responseHeaderTimeout time.Duration
|
responseHeaderTimeout time.Duration
|
||||||
}
|
}
|
||||||
@@ -55,6 +59,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
|
|||||||
rp := &HTTPReverseProxy{
|
rp := &HTTPReverseProxy{
|
||||||
responseHeaderTimeout: time.Duration(option.ResponseHeaderTimeoutS) * time.Second,
|
responseHeaderTimeout: time.Duration(option.ResponseHeaderTimeoutS) * time.Second,
|
||||||
vhostRouter: vhostRouter,
|
vhostRouter: vhostRouter,
|
||||||
|
httpsRedirector: option.HTTPSRedirector,
|
||||||
}
|
}
|
||||||
proxy := &httputil.ReverseProxy{
|
proxy := &httputil.ReverseProxy{
|
||||||
// Modify incoming requests by route policies.
|
// Modify incoming requests by route policies.
|
||||||
@@ -281,6 +286,12 @@ func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
|||||||
return
|
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 {
|
if req.Method == http.MethodConnect {
|
||||||
rp.connectHandler(rw, newreq)
|
rp.connectHandler(rw, newreq)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
83
pkg/util/vhost/https_redirect.go
Normal file
83
pkg/util/vhost/https_redirect.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
46
pkg/util/vhost/https_redirect_test.go
Normal file
46
pkg/util/vhost/https_redirect_test.go
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
@@ -50,6 +50,9 @@ type ResourceController struct {
|
|||||||
// For HTTP proxies, forwarding HTTP requests
|
// For HTTP proxies, forwarding HTTP requests
|
||||||
HTTPReverseProxy *vhost.HTTPReverseProxy
|
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
|
// For HTTPS proxies, route requests to different clients by hostname and other information
|
||||||
VhostHTTPSMuxer *vhost.HTTPSMuxer
|
VhostHTTPSMuxer *vhost.HTTPSMuxer
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ func init() {
|
|||||||
type HTTPSProxy struct {
|
type HTTPSProxy struct {
|
||||||
*BaseProxy
|
*BaseProxy
|
||||||
cfg *v1.HTTPSProxyConfig
|
cfg *v1.HTTPSProxyConfig
|
||||||
|
|
||||||
|
// domains registered for HTTP to HTTPS redirection, removed on Close
|
||||||
|
redirectDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPSProxy(baseProxy *BaseProxy) Proxy {
|
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)
|
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()
|
pxy.startCommonTCPListenersHandler()
|
||||||
remoteAddr = strings.Join(addrs, ",")
|
remoteAddr = strings.Join(addrs, ",")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *HTTPSProxy) Close() {
|
func (pxy *HTTPSProxy) Close() {
|
||||||
|
for _, domain := range pxy.redirectDomains {
|
||||||
|
pxy.rc.HTTPSRedirector.Remove(domain)
|
||||||
|
}
|
||||||
|
pxy.redirectDomains = nil
|
||||||
pxy.BaseProxy.Close()
|
pxy.BaseProxy.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
server/proxy/idle_watcher.go
Normal file
47
server/proxy/idle_watcher.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
52
server/proxy/idle_watcher_test.go
Normal file
52
server/proxy/idle_watcher_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
@@ -317,15 +317,33 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
|
|
||||||
name := pxy.GetName()
|
name := pxy.GetName()
|
||||||
proxyType := cfg.Type
|
proxyType := cfg.Type
|
||||||
|
var watcher *idleWatcher
|
||||||
|
touch := func() {
|
||||||
|
if watcher != nil {
|
||||||
|
watcher.touch()
|
||||||
|
}
|
||||||
|
}
|
||||||
local = wrapCountingReadWriteCloser(local, nil, func(bytes int64) {
|
local = wrapCountingReadWriteCloser(local, nil, func(bytes int64) {
|
||||||
|
touch()
|
||||||
metrics.Server.AddTrafficIn(name, proxyType, bytes)
|
metrics.Server.AddTrafficIn(name, proxyType, bytes)
|
||||||
})
|
})
|
||||||
userConn = netpkg.WrapReadWriteCloserToConn(
|
userConn = netpkg.WrapReadWriteCloserToConn(
|
||||||
wrapCountingReadWriteCloser(userConn, nil, func(bytes int64) {
|
wrapCountingReadWriteCloser(userConn, nil, func(bytes int64) {
|
||||||
|
touch()
|
||||||
metrics.Server.AddTrafficOut(name, proxyType, bytes)
|
metrics.Server.AddTrafficOut(name, proxyType, bytes)
|
||||||
}),
|
}),
|
||||||
userConn,
|
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)
|
metrics.Server.OpenConnection(name, proxyType)
|
||||||
// Traffic is counted incrementally via the counting wrappers above, so the
|
// Traffic is counted incrementally via the counting wrappers above, so the
|
||||||
// byte totals returned by joinUserConnection are intentionally discarded here.
|
// byte totals returned by joinUserConnection are intentionally discarded here.
|
||||||
|
|||||||
@@ -291,8 +291,14 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
|
|
||||||
// Create http vhost muxer.
|
// Create http vhost muxer.
|
||||||
if cfg.VhostHTTPPort > 0 {
|
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{
|
rp := vhost.NewHTTPReverseProxy(vhost.HTTPReverseProxyOptions{
|
||||||
ResponseHeaderTimeoutS: cfg.VhostHTTPTimeout,
|
ResponseHeaderTimeoutS: cfg.VhostHTTPTimeout,
|
||||||
|
HTTPSRedirector: redirector,
|
||||||
}, svr.httpVhostRouter)
|
}, svr.httpVhostRouter)
|
||||||
svr.rc.HTTPReverseProxy = rp
|
svr.rc.HTTPReverseProxy = rp
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user