forked from Mxmilu666/frp
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0ff5557571
|
|||
|
bce3d5a3d8
|
|||
|
33a29b04e4
|
|||
|
481ccad05e
|
|||
|
521542b796
|
|||
|
e487e7f96f
|
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
|
||||||
- name: Generate image digest
|
- name: Generate image digest
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -98,4 +98,5 @@ 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
|
// Use the Lolia-FRP fork of golib: io.Join relays with adaptively sized buffers.
|
||||||
|
replace github.com/fatedier/golib => github.com/Lolia-FRP/golib v0.0.0-20260704205217-7f676961e707
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -2,8 +2,8 @@ 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.0.0-20260704205217-7f676961e707 h1:ROQVjgk+RvaN8J8uR8ekUu4TKgJLXYObVeQC/0KADn4=
|
||||||
github.com/Lolia-FRP/golib v0.7.4/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
github.com/Lolia-FRP/golib v0.0.0-20260704205217-7f676961e707/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=
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ func NewHTTP2HTTPSRedirectPlugin(_ PluginContext, options v1.ClientPluginOptions
|
|||||||
|
|
||||||
p.s = &http.Server{
|
p.s = &http.Server{
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
http.Redirect(w, req, buildHTTPSRedirectURL(req, opts.HTTPSPort), http.StatusFound)
|
// Not an open redirect: the target scheme is fixed to https and the
|
||||||
|
// host is the one the client already connected to.
|
||||||
|
http.Redirect(w, req, buildHTTPSRedirectURL(req, opts.HTTPSPort), http.StatusFound) //nolint:gosec // G710
|
||||||
}),
|
}),
|
||||||
ReadHeaderTimeout: 60 * time.Second,
|
ReadHeaderTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func (w *logCapture) String() string {
|
|||||||
// levels parses the captured JSON log output and returns the level of each entry.
|
// levels parses the captured JSON log output and returns the level of each entry.
|
||||||
func (w *logCapture) levels() []charmlog.Level {
|
func (w *logCapture) levels() []charmlog.Level {
|
||||||
var levels []charmlog.Level
|
var levels []charmlog.Level
|
||||||
for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") {
|
for line := range strings.SplitSeq(strings.TrimSpace(w.String()), "\n") {
|
||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,9 +164,8 @@ func (w *RotateFileWriter) cleanupOldLogs(now time.Time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
if strings.HasPrefix(name, base+".") {
|
// Extract date from filename (base.YYYY-MM-DD)
|
||||||
// Extract date from filename (base.YYYY-MM-DD)
|
if dateStr, ok := strings.CutPrefix(name, base+"."); ok {
|
||||||
dateStr := strings.TrimPrefix(name, base+".")
|
|
||||||
if len(dateStr) == 10 {
|
if len(dateStr) == 10 {
|
||||||
fileDate, err := time.Parse("2006-01-02", dateStr)
|
fileDate, err := time.Parse("2006-01-02", dateStr)
|
||||||
if err == nil && fileDate.Before(cutoffDate) {
|
if err == nil && fileDate.Before(cutoffDate) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var version = "LoliaFRP-CLI 0.69.1"
|
var version = "LoliaFRP-CLI 0.69.3"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
93
pkg/util/vhost/https_redirect.go
Normal file
93
pkg/util/vhost/https_redirect.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// 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"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"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 := url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: host,
|
||||||
|
Path: req.URL.Path,
|
||||||
|
RawPath: req.URL.RawPath,
|
||||||
|
RawQuery: req.URL.RawQuery,
|
||||||
|
}
|
||||||
|
if r.port != 443 {
|
||||||
|
target.Host = net.JoinHostPort(host, strconv.Itoa(r.port))
|
||||||
|
}
|
||||||
|
// Not an open redirect: the host is validated against the registered
|
||||||
|
// domain set above, the scheme is fixed, and only the same-host path and
|
||||||
|
// query are echoed back.
|
||||||
|
http.Redirect(rw, req, target.String(), http.StatusMovedPermanently) //nolint:gosec // G710
|
||||||
|
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