forked from Mxmilu666/frp
feat(proxy): add HTTP to HTTPS redirection support
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user