mirror of
https://github.com/fatedier/frp.git
synced 2026-04-27 03:19:11 +08:00
plugin/http_proxy: fix fragmented CONNECT method detection and add read deadline (#5296)
This commit is contained in:
@@ -8,9 +8,9 @@ For mixed-version deployments, upgrade frps first, then upgrade frpc. This keeps
|
||||
|
||||
This release introduces wire protocol v2 as a transition path for future frpc/frps protocol changes. The existing wire protocol is difficult to extend without compatibility risk, and upcoming changes, including replacing deprecated stream encryption methods, require a versioned protocol.
|
||||
|
||||
**The default value of `transport.wireProtocol` remains `v1` in this release, but it will switch to `v2` in the next release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps.
|
||||
**The default value of `transport.wireProtocol` remains `v1` in this release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps.
|
||||
|
||||
v1 will be deprecated when v2 becomes the default in the next release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later.
|
||||
v1 will be deprecated when v2 becomes the default in a future release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ type HTTPProxy struct {
|
||||
s *http.Server
|
||||
}
|
||||
|
||||
const httpProxyReadHeaderTimeout = 60 * time.Second
|
||||
|
||||
func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
opts := options.(*v1.HTTPProxyPluginOptions)
|
||||
listener := NewProxyListener()
|
||||
@@ -56,7 +58,7 @@ func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin
|
||||
|
||||
hp.s = &http.Server{
|
||||
Handler: hp,
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
ReadHeaderTimeout: httpProxyReadHeaderTimeout,
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -73,16 +75,19 @@ func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) {
|
||||
wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
|
||||
|
||||
sc, rd := libnet.NewSharedConn(wrapConn)
|
||||
firstBytes := make([]byte, 7)
|
||||
_, err := rd.Read(firstBytes)
|
||||
firstBytes := make([]byte, len(http.MethodConnect))
|
||||
_ = wrapConn.SetReadDeadline(time.Now().Add(httpProxyReadHeaderTimeout))
|
||||
_, err := io.ReadFull(rd, firstBytes)
|
||||
if err != nil {
|
||||
_ = wrapConn.SetReadDeadline(time.Time{})
|
||||
wrapConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ToUpper(string(firstBytes)) == "CONNECT" {
|
||||
if strings.EqualFold(string(firstBytes), http.MethodConnect) {
|
||||
bufRd := bufio.NewReader(sc)
|
||||
request, err := http.ReadRequest(bufRd)
|
||||
_ = wrapConn.SetReadDeadline(time.Time{})
|
||||
if err != nil {
|
||||
wrapConn.Close()
|
||||
return
|
||||
@@ -91,6 +96,7 @@ func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = wrapConn.SetReadDeadline(time.Time{})
|
||||
_ = hp.l.PutConn(sc)
|
||||
}
|
||||
|
||||
|
||||
107
pkg/plugin/client/http_proxy_test.go
Normal file
107
pkg/plugin/client/http_proxy_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright 2026 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.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestHTTPProxyHandleFragmentedConnectMethod(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(err)
|
||||
defer ln.Close()
|
||||
|
||||
const payload = "ping"
|
||||
echoErr := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
echoErr <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
buf := make([]byte, len(payload))
|
||||
if _, err = io.ReadFull(conn, buf); err != nil {
|
||||
echoErr <- err
|
||||
return
|
||||
}
|
||||
if string(buf) != payload {
|
||||
echoErr <- fmt.Errorf("unexpected payload %q", string(buf))
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("echo:" + payload))
|
||||
echoErr <- err
|
||||
}()
|
||||
|
||||
hp := &HTTPProxy{
|
||||
opts: &v1.HTTPProxyPluginOptions{},
|
||||
l: NewProxyListener(),
|
||||
}
|
||||
|
||||
clientConn, serverConn := net.Pipe()
|
||||
defer clientConn.Close()
|
||||
|
||||
go hp.Handle(context.Background(), &ConnectionInfo{
|
||||
Conn: serverConn,
|
||||
UnderlyingConn: serverConn,
|
||||
})
|
||||
|
||||
require.NoError(clientConn.SetDeadline(time.Now().Add(5 * time.Second)))
|
||||
|
||||
targetAddr := ln.Addr().String()
|
||||
req := "CONNECT " + targetAddr + " HTTP/1.1\r\nHost: " + targetAddr + "\r\n\r\n"
|
||||
_, err = clientConn.Write([]byte("CON"))
|
||||
require.NoError(err)
|
||||
_, err = clientConn.Write([]byte(req[len("CON"):]))
|
||||
require.NoError(err)
|
||||
|
||||
rd := bufio.NewReader(clientConn)
|
||||
status, err := rd.ReadString('\n')
|
||||
require.NoError(err)
|
||||
require.Equal("HTTP/1.1 200 OK\r\n", status)
|
||||
line, err := rd.ReadString('\n')
|
||||
require.NoError(err)
|
||||
require.Equal("\r\n", line)
|
||||
|
||||
_, err = clientConn.Write([]byte(payload))
|
||||
require.NoError(err)
|
||||
|
||||
got := make([]byte, len("echo:"+payload))
|
||||
_, err = io.ReadFull(rd, got)
|
||||
require.NoError(err)
|
||||
require.Equal("echo:"+payload, string(got))
|
||||
|
||||
select {
|
||||
case err := <-echoErr:
|
||||
require.NoError(err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for echo server")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user