diff --git a/Release.md b/Release.md index a1fa993..2dcfc56 100644 --- a/Release.md +++ b/Release.md @@ -1,9 +1,3 @@ -### 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. - -### 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. +* 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. 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/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go index 3a7c834..333b020 100644 --- a/pkg/config/v1/plugin.go +++ b/pkg/config/v1/plugin.go @@ -76,6 +76,7 @@ const ( PluginSocks5 = "socks5" PluginStaticFile = "static_file" PluginUnixDomainSocket = "unix_domain_socket" + PluginHTTP2HTTP = "http2http" ) var clientPluginOptionsTypeMap = map[string]reflect.Type{ @@ -86,6 +87,7 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{ PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), + PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}), } type HTTP2HTTPSPluginOptions struct { @@ -137,3 +139,11 @@ type UnixDomainSocketPluginOptions struct { Type string `json:"type,omitempty"` UnixPath string `json:"unixPath,omitempty"` } + +// Added HTTP2HTTPPluginOptions struct +type HTTP2HTTPPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` +} 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/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() + }) + }) })