Просмотр исходного кода

Add http2http client plugin with hostHeaderRewrite and requestHeaders support (#4275)

fatedier 9 месяцев назад
Родитель
Сommit
939c490768
5 измененных файлов с 186 добавлено и 8 удалено
  1. 2 8
      Release.md
  2. 10 0
      conf/frpc_full_example.toml
  3. 10 0
      pkg/config/v1/plugin.go
  4. 91 0
      pkg/plugin/client/http2http.go
  5. 73 0
      test/e2e/v1/plugin/client.go

+ 2 - 8
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.

+ 10 - 0
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

+ 10 - 0
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"`
+}

+ 91 - 0
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()
+}

+ 73 - 0
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()
+		})
+	})
 })