diff --git a/README.md b/README.md
index 9122de1e..915e1e8e 100644
--- a/README.md
+++ b/README.md
@@ -10,12 +10,23 @@
Gold Sponsors
-
-
+
+
-  
+
+
+
+
+
+
+
-
+
+
+
+
+
+
diff --git a/README_zh.md b/README_zh.md
index f71a5794..95ede3ee 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 a1fa993a..6e142def 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 c88087a1..51b89c2a 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 d8fef587..00000000
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 b00f9327..a5271cc9 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 0d66038b..00000000
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 00000000..82386356
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 4b835656..00000000
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 00000000..798302ab
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 3a7c8344..7ae4a4d4 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 45c489f6..d53d05e3 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 00000000..689b90b6
--- /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 6d686361..3bb12c22 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 5ddd4dd6..c315c8e3 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 11d140eb..561a52e7 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 476adb89..3499e882 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()
+ })
+ })
})