Browse Source

Merge pull request #4324 from fatedier/dev

bump version
fatedier 6 months ago
parent
commit
243ca994e0

+ 14 - 3
README.md

@@ -9,13 +9,24 @@
 
 <h3 align="center">Gold Sponsors</h3>
 <!--gold sponsors start-->
+<p align="center">
+  <a href="https://lokal.so/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_lokal.png">
+  </a>
+</p>
 <p align="center">
   <a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
-    <img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
   </a>
-  <a>&nbsp</a>
+</p>
+<p align="center">
   <a href="https://github.com/daytonaio/daytona" target="_blank">
-    <img width="360px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
+  </a>
+</p>
+<p align="center">
+  <a href="https://github.com/beclab/terminus" target="_blank">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_terminusos.jpeg">
   </a>
 </p>
 <!--gold sponsors end-->

+ 14 - 3
README_zh.md

@@ -11,13 +11,24 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
 
 <h3 align="center">Gold Sponsors</h3>
 <!--gold sponsors start-->
+<p align="center">
+  <a href="https://lokal.so/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_lokal.png">
+  </a>
+</p>
 <p align="center">
   <a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
-    <img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
   </a>
-  <a>&nbsp</a>
+</p>
+<p align="center">
   <a href="https://github.com/daytonaio/daytona" target="_blank">
-    <img width="360px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
+  </a>
+</p>
+<p align="center">
+  <a href="https://github.com/beclab/terminus" target="_blank">
+    <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_terminusos.jpeg">
   </a>
 </p>
 <!--gold sponsors end-->

+ 4 - 5
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.

+ 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

BIN
doc/pic/donate-wechatpay.png


BIN
doc/pic/sponsor_daytona.png


BIN
doc/pic/sponsor_doppler.png


BIN
doc/pic/sponsor_lokal.png


BIN
doc/pic/sponsor_nango.png


BIN
doc/pic/sponsor_terminusos.jpeg


+ 38 - 1
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() {}

+ 4 - 0
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) {

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

+ 17 - 1
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, "", "")

+ 17 - 1
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, "", "")

+ 1 - 1
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

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