diff --git a/README.md b/README.md index 9122de1..915e1e8 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,23 @@

Gold Sponsors

- - + + -   +

+

+ + + +

+

- + + +

+

+ +

diff --git a/README_zh.md b/README_zh.md index f71a579..95ede3e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -12,12 +12,23 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP

Gold Sponsors

- - + + -   +

+

+ + + +

+

- + + +

+

+ +

diff --git a/Release.md b/Release.md index a1fa993..6e142de 100644 --- a/Release.md +++ b/Release.md @@ -1,9 +1,8 @@ -### Fixes +### Features -* Fixed an issue where HTTP/2 was not enabled for https2http and https2https plugins. -* Fixed the issue where the default values of INI configuration parameters are inconsistent with other configuration formats. +* Added a new plugin "http2http" which allows forwarding HTTP requests to another HTTP server, supporting options like local address binding, host header rewrite, and custom request headers. +* Added `enableHTTP2` option to control whether to enable HTTP/2 in plugin https2http and https2https, default is true. ### Changes -* Updated the default value of `transport.tcpMuxKeepaliveInterval` from 60 to 30. -* On the Android platform, the Google DNS server is used only when the default DNS server cannot be obtained. +* Plugin https2http & https2https: return 421 `Misdirected Request` if host not match sni. diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index c88087a..51b89c2 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -315,6 +315,16 @@ localAddr = "127.0.0.1:443" hostHeaderRewrite = "127.0.0.1" requestHeaders.set.x-from-where = "frp" +[[proxies]] +name = "plugin_http2http" +type = "tcp" +remotePort = 6007 +[proxies.plugin] +type = "http2http" +localAddr = "127.0.0.1:80" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + [[proxies]] name = "secret_tcp" # If the type is secret tcp, remotePort is useless diff --git a/doc/pic/donate-wechatpay.png b/doc/pic/donate-wechatpay.png deleted file mode 100644 index d8fef58..0000000 Binary files a/doc/pic/donate-wechatpay.png and /dev/null differ diff --git a/doc/pic/sponsor_daytona.png b/doc/pic/sponsor_daytona.png index b00f932..a5271cc 100644 Binary files a/doc/pic/sponsor_daytona.png and b/doc/pic/sponsor_daytona.png differ diff --git a/doc/pic/sponsor_doppler.png b/doc/pic/sponsor_doppler.png deleted file mode 100644 index 0d66038..0000000 Binary files a/doc/pic/sponsor_doppler.png and /dev/null differ diff --git a/doc/pic/sponsor_lokal.png b/doc/pic/sponsor_lokal.png new file mode 100644 index 0000000..8238635 Binary files /dev/null and b/doc/pic/sponsor_lokal.png differ diff --git a/doc/pic/sponsor_nango.png b/doc/pic/sponsor_nango.png deleted file mode 100644 index 4b83565..0000000 Binary files a/doc/pic/sponsor_nango.png and /dev/null differ diff --git a/doc/pic/sponsor_terminusos.jpeg b/doc/pic/sponsor_terminusos.jpeg new file mode 100644 index 0000000..798302a Binary files /dev/null and b/doc/pic/sponsor_terminusos.jpeg differ diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go index 3a7c834..7ae4a4d 100644 --- a/pkg/config/v1/plugin.go +++ b/pkg/config/v1/plugin.go @@ -20,9 +20,15 @@ import ( "errors" "fmt" "reflect" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/util/util" ) -type ClientPluginOptions interface{} +type ClientPluginOptions interface { + Complete() +} type TypedClientPluginOptions struct { Type string `json:"type"` @@ -73,6 +79,7 @@ const ( PluginHTTPProxy = "http_proxy" PluginHTTPS2HTTP = "https2http" PluginHTTPS2HTTPS = "https2https" + PluginHTTP2HTTP = "http2http" PluginSocks5 = "socks5" PluginStaticFile = "static_file" PluginUnixDomainSocket = "unix_domain_socket" @@ -83,6 +90,7 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{ 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{}), @@ -95,36 +103,61 @@ type HTTP2HTTPSPluginOptions struct { RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` } +func (o *HTTP2HTTPSPluginOptions) Complete() {} + type HTTPProxyPluginOptions struct { Type string `json:"type,omitempty"` HTTPUser string `json:"httpUser,omitempty"` HTTPPassword string `json:"httpPassword,omitempty"` } +func (o *HTTPProxyPluginOptions) Complete() {} + type HTTPS2HTTPPluginOptions struct { Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + EnableHTTP2 *bool `json:"enableHTTP2,omitempty"` CrtPath string `json:"crtPath,omitempty"` KeyPath string `json:"keyPath,omitempty"` } +func (o *HTTPS2HTTPPluginOptions) Complete() { + o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true)) +} + type HTTPS2HTTPSPluginOptions struct { Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + EnableHTTP2 *bool `json:"enableHTTP2,omitempty"` CrtPath string `json:"crtPath,omitempty"` KeyPath string `json:"keyPath,omitempty"` } +func (o *HTTPS2HTTPSPluginOptions) Complete() { + o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true)) +} + +type HTTP2HTTPPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` +} + +func (o *HTTP2HTTPPluginOptions) Complete() {} + type Socks5PluginOptions struct { Type string `json:"type,omitempty"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` } +func (o *Socks5PluginOptions) Complete() {} + type StaticFilePluginOptions struct { Type string `json:"type,omitempty"` LocalPath string `json:"localPath,omitempty"` @@ -133,7 +166,11 @@ type StaticFilePluginOptions struct { HTTPPassword string `json:"httpPassword,omitempty"` } +func (o *StaticFilePluginOptions) Complete() {} + type UnixDomainSocketPluginOptions struct { Type string `json:"type,omitempty"` UnixPath string `json:"unixPath,omitempty"` } + +func (o *UnixDomainSocketPluginOptions) Complete() {} diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 45c489f..d53d05e 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -127,6 +127,10 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) { c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1") c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient) + + if c.Plugin.ClientPluginOptions != nil { + c.Plugin.ClientPluginOptions.Complete() + } } func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) { diff --git a/pkg/plugin/client/http2http.go b/pkg/plugin/client/http2http.go new file mode 100644 index 0000000..689b90b --- /dev/null +++ b/pkg/plugin/client/http2http.go @@ -0,0 +1,91 @@ +// Copyright 2024 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 plugin + +import ( + "io" + stdlog "log" + "net" + "net/http" + "net/http/httputil" + + "github.com/fatedier/golib/pool" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +func init() { + Register(v1.PluginHTTP2HTTP, NewHTTP2HTTPPlugin) +} + +type HTTP2HTTPPlugin struct { + opts *v1.HTTP2HTTPPluginOptions + + l *Listener + s *http.Server +} + +func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTP2HTTPPluginOptions) + + listener := NewProxyListener() + + p := &HTTP2HTTPPlugin{ + opts: opts, + l: listener, + } + + rp := &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + req := r.Out + req.URL.Scheme = "http" + req.URL.Host = p.opts.LocalAddr + if p.opts.HostHeaderRewrite != "" { + req.Host = p.opts.HostHeaderRewrite + } + for k, v := range p.opts.RequestHeaders.Set { + req.Header.Set(k, v) + } + }, + BufferPool: pool.NewBuffer(32 * 1024), + ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0), + } + + p.s = &http.Server{ + Handler: rp, + ReadHeaderTimeout: 0, + } + + go func() { + _ = p.s.Serve(listener) + }() + + return p, nil +} + +func (p *HTTP2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) + _ = p.l.PutConn(wrapConn) +} + +func (p *HTTP2HTTPPlugin) Name() string { + return v1.PluginHTTP2HTTP +} + +func (p *HTTP2HTTPPlugin) Close() error { + return p.s.Close() +} diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go index 6d68636..3bb12c2 100644 --- a/pkg/plugin/client/https2http.go +++ b/pkg/plugin/client/https2http.go @@ -27,9 +27,11 @@ import ( "time" "github.com/fatedier/golib/pool" + "github.com/samber/lo" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" netpkg "github.com/fatedier/frp/pkg/util/net" ) @@ -71,6 +73,17 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { BufferPool: pool.NewBuffer(32 * 1024), ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0), } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.TLS != nil { + tlsServerName, _ := httppkg.CanonicalHost(r.TLS.ServerName) + host, _ := httppkg.CanonicalHost(r.Host) + if tlsServerName != "" && tlsServerName != host { + w.WriteHeader(http.StatusMisdirectedRequest) + return + } + } + rp.ServeHTTP(w, r) + }) var ( tlsConfig *tls.Config @@ -87,10 +100,13 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { } p.s = &http.Server{ - Handler: rp, + Handler: handler, ReadHeaderTimeout: 60 * time.Second, TLSConfig: tlsConfig, } + if !lo.FromPtr(opts.EnableHTTP2) { + p.s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + } go func() { _ = p.s.ServeTLS(listener, "", "") diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go index 5ddd4dd..c315c8e 100644 --- a/pkg/plugin/client/https2https.go +++ b/pkg/plugin/client/https2https.go @@ -27,9 +27,11 @@ import ( "time" "github.com/fatedier/golib/pool" + "github.com/samber/lo" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" netpkg "github.com/fatedier/frp/pkg/util/net" ) @@ -77,6 +79,17 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { BufferPool: pool.NewBuffer(32 * 1024), ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0), } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.TLS != nil { + tlsServerName, _ := httppkg.CanonicalHost(r.TLS.ServerName) + host, _ := httppkg.CanonicalHost(r.Host) + if tlsServerName != "" && tlsServerName != host { + w.WriteHeader(http.StatusMisdirectedRequest) + return + } + } + rp.ServeHTTP(w, r) + }) var ( tlsConfig *tls.Config @@ -93,10 +106,13 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { } p.s = &http.Server{ - Handler: rp, + Handler: handler, ReadHeaderTimeout: 60 * time.Second, TLSConfig: tlsConfig, } + if !lo.FromPtr(opts.EnableHTTP2) { + p.s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + } go func() { _ = p.s.ServeTLS(listener, "", "") diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 11d140e..561a52e 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.58.1" +var version = "0.59.0" func Full() string { return version diff --git a/test/e2e/v1/plugin/client.go b/test/e2e/v1/plugin/client.go index 476adb8..3499e88 100644 --- a/test/e2e/v1/plugin/client.go +++ b/test/e2e/v1/plugin/client.go @@ -3,6 +3,7 @@ package plugin import ( "crypto/tls" "fmt" + "net/http" "strconv" "github.com/onsi/ginkgo/v2" @@ -329,4 +330,76 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { ExpectResp([]byte("test")). Ensure() }) + + ginkgo.Describe("http2http", func() { + ginkgo.It("host header rewrite", func() { + serverConf := consts.DefaultServerConfig + + localPort := f.AllocPort() + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http2http" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "http2http" + localAddr = "127.0.0.1:%d" + hostHeaderRewrite = "rewrite.test.com" + `, remotePort, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Host)) + })), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(remotePort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + ExpectResp([]byte("rewrite.test.com")). + Ensure() + }) + + ginkgo.It("set request header", func() { + serverConf := consts.DefaultServerConfig + + localPort := f.AllocPort() + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http2http" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "http2http" + localAddr = "127.0.0.1:%d" + requestHeaders.set.x-from-where = "frp" + `, remotePort, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("x-from-where"))) + })), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(remotePort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + ExpectResp([]byte("frp")). + Ensure() + }) + }) })