diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 6dbcf6d5..54bbf73e 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -370,6 +370,15 @@ localAddr = "127.0.0.1:443" hostHeaderRewrite = "127.0.0.1" requestHeaders.set.x-from-where = "frp" +[[proxies]] +name = "plugin_http2https_redirect" +type = "http" +customDomains = ["test.yourdomain.com"] +[proxies.plugin] +type = "http2https_redirect" +# Optional. Defaults to 443. Set this if the HTTPS entry is exposed on a non-standard port. +# httpsPort = 443 + [[proxies]] name = "plugin_http2http" type = "tcp" diff --git a/pkg/config/v1/proxy_plugin.go b/pkg/config/v1/proxy_plugin.go index 07825140..42c89341 100644 --- a/pkg/config/v1/proxy_plugin.go +++ b/pkg/config/v1/proxy_plugin.go @@ -27,29 +27,31 @@ import ( ) const ( - PluginHTTP2HTTPS = "http2https" - PluginHTTPProxy = "http_proxy" - PluginHTTPS2HTTP = "https2http" - PluginHTTPS2HTTPS = "https2https" - PluginHTTP2HTTP = "http2http" - PluginSocks5 = "socks5" - PluginStaticFile = "static_file" - PluginUnixDomainSocket = "unix_domain_socket" - PluginTLS2Raw = "tls2raw" - PluginVirtualNet = "virtual_net" + PluginHTTP2HTTPS = "http2https" + PluginHTTP2HTTPSRedirect = "http2https_redirect" + PluginHTTPProxy = "http_proxy" + PluginHTTPS2HTTP = "https2http" + PluginHTTPS2HTTPS = "https2https" + PluginHTTP2HTTP = "http2http" + PluginSocks5 = "socks5" + PluginStaticFile = "static_file" + PluginUnixDomainSocket = "unix_domain_socket" + PluginTLS2Raw = "tls2raw" + PluginVirtualNet = "virtual_net" ) var clientPluginOptionsTypeMap = map[string]reflect.Type{ - PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}), - PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}), - PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}), - PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}), - PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}), - PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), - PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), - PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), - PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}), - PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}), + PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}), + PluginHTTP2HTTPSRedirect: reflect.TypeOf(HTTP2HTTPSRedirectPluginOptions{}), + PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}), + PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}), + PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}), + PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}), + PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), + PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), + PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), + PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}), + PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}), } type ClientPluginOptions interface { @@ -109,6 +111,13 @@ type HTTP2HTTPSPluginOptions struct { func (o *HTTP2HTTPSPluginOptions) Complete() {} +type HTTP2HTTPSRedirectPluginOptions struct { + Type string `json:"type,omitempty"` + HTTPSPort int `json:"httpsPort,omitempty"` +} + +func (o *HTTP2HTTPSRedirectPluginOptions) Complete() {} + type HTTPProxyPluginOptions struct { Type string `json:"type,omitempty"` HTTPUser string `json:"httpUser,omitempty"` diff --git a/pkg/config/v1/validation/plugin.go b/pkg/config/v1/validation/plugin.go index fb66be36..206ff4b3 100644 --- a/pkg/config/v1/validation/plugin.go +++ b/pkg/config/v1/validation/plugin.go @@ -26,6 +26,8 @@ func ValidateClientPluginOptions(c v1.ClientPluginOptions) error { switch v := c.(type) { case *v1.HTTP2HTTPSPluginOptions: return validateHTTP2HTTPSPluginOptions(v) + case *v1.HTTP2HTTPSRedirectPluginOptions: + return validateHTTP2HTTPSRedirectPluginOptions(v) case *v1.HTTPS2HTTPPluginOptions: return validateHTTPS2HTTPPluginOptions(v) case *v1.HTTPS2HTTPSPluginOptions: @@ -47,6 +49,10 @@ func validateHTTP2HTTPSPluginOptions(c *v1.HTTP2HTTPSPluginOptions) error { return nil } +func validateHTTP2HTTPSRedirectPluginOptions(c *v1.HTTP2HTTPSRedirectPluginOptions) error { + return ValidatePort(c.HTTPSPort, "httpsPort") +} + func validateHTTPS2HTTPPluginOptions(c *v1.HTTPS2HTTPPluginOptions) error { if c.LocalAddr == "" { return errors.New("localAddr is required") diff --git a/pkg/plugin/client/autotls.go b/pkg/plugin/client/autotls.go index 75697172..7b012087 100644 --- a/pkg/plugin/client/autotls.go +++ b/pkg/plugin/client/autotls.go @@ -1,4 +1,4 @@ -// Copyright 2026 The frp Authors +// Copyright 2026 The LoliaTeam Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/plugin/client/http2https_redirect.go b/pkg/plugin/client/http2https_redirect.go new file mode 100644 index 00000000..c3ad02fa --- /dev/null +++ b/pkg/plugin/client/http2https_redirect.go @@ -0,0 +1,107 @@ +// Copyright 2026 The LoliaTeam 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. + +//go:build !frps + +package client + +import ( + "context" + "net" + "net/http" + "net/url" + "strconv" + "strings" + + v1 "github.com/fatedier/frp/pkg/config/v1" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +func init() { + Register(v1.PluginHTTP2HTTPSRedirect, NewHTTP2HTTPSRedirectPlugin) +} + +type HTTP2HTTPSRedirectPlugin struct { + opts *v1.HTTP2HTTPSRedirectPluginOptions + + l *Listener + s *http.Server +} + +func NewHTTP2HTTPSRedirectPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTP2HTTPSRedirectPluginOptions) + + listener := NewProxyListener() + p := &HTTP2HTTPSRedirectPlugin{ + opts: opts, + l: listener, + } + + p.s = &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, buildHTTPSRedirectURL(req, opts.HTTPSPort), http.StatusFound) + }), + } + + go func() { + _ = p.s.Serve(listener) + }() + + return p, nil +} + +func buildHTTPSRedirectURL(req *http.Request, httpsPort int) string { + host := strings.TrimSpace(req.Host) + if host == "" { + host = strings.TrimSpace(req.URL.Host) + } + + targetHost := host + if parsedHost, parsedPort, err := net.SplitHostPort(host); err == nil { + targetHost = parsedHost + if httpsPort == 0 && parsedPort == "443" { + httpsPort = 443 + } + } else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + targetHost = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]") + } + + if httpsPort != 0 && httpsPort != 443 { + targetHost = net.JoinHostPort(targetHost, strconv.Itoa(httpsPort)) + } + + return (&url.URL{ + Scheme: "https", + Host: targetHost, + Path: req.URL.Path, + RawPath: req.URL.RawPath, + RawQuery: req.URL.RawQuery, + }).String() +} + +func (p *HTTP2HTTPSRedirectPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) + if connInfo.SrcAddr != nil { + wrapConn.SetRemoteAddr(connInfo.SrcAddr) + } + _ = p.l.PutConn(wrapConn) +} + +func (p *HTTP2HTTPSRedirectPlugin) Name() string { + return v1.PluginHTTP2HTTPSRedirect +} + +func (p *HTTP2HTTPSRedirectPlugin) Close() error { + return p.s.Close() +}