Files
frp/pkg/util/vhost/http_test.go
fatedier f8fc3c6b1b server: drop HTTP/1.1 h2c upgrade handling (#5436)
Use net/http Server.Protocols and the merged golib listener path instead of the deprecated h2c handler. Keep HTTP/1.1 and cleartext HTTP/2 prior-knowledge support, while intentionally dropping HTTP/1.1 Upgrade: h2c.
2026-07-22 19:55:42 +08:00

183 lines
6.0 KiB
Go

package vhost
import (
"bufio"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
httppkg "github.com/fatedier/frp/pkg/util/http"
)
func TestHTTPServerProtocols(t *testing.T) {
rp := NewHTTPReverseProxy(HTTPReverseProxyOptions{}, NewRouters())
protocols := new(http.Protocols)
protocols.SetHTTP1(true)
protocols.SetUnencryptedHTTP2(true)
server := &http.Server{
Handler: rp,
ReadHeaderTimeout: time.Second,
Protocols: protocols,
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
serveErr := make(chan error, 1)
go func() {
serveErr <- server.Serve(listener)
}()
defer func() {
require.NoError(t, server.Close())
require.ErrorIs(t, <-serveErr, http.ErrServerClosed)
}()
require.True(t, server.Protocols.HTTP1())
require.True(t, server.Protocols.UnencryptedHTTP2())
t.Run("HTTP/1.1", func(t *testing.T) {
transport := &http.Transport{Protocols: httpProtocols(true, false)}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport}
response, err := client.Get("http://" + listener.Addr().String() + "/")
require.NoError(t, err)
defer response.Body.Close()
require.Equal(t, "HTTP/1.1", response.Proto)
require.Equal(t, http.StatusNotFound, response.StatusCode)
})
t.Run("HTTP/2 prior knowledge", func(t *testing.T) {
transport := &http.Transport{Protocols: httpProtocols(false, true)}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport}
response, err := client.Get("http://" + listener.Addr().String() + "/")
require.NoError(t, err)
defer response.Body.Close()
require.Equal(t, "HTTP/2.0", response.Proto)
require.Equal(t, http.StatusNotFound, response.StatusCode)
})
t.Run("HTTP/1.1 Upgrade h2c", func(t *testing.T) {
conn, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err)
defer conn.Close()
_, err = fmt.Fprintf(conn,
"GET / HTTP/1.1\r\nHost: %s\r\n"+
"Connection: Upgrade, HTTP2-Settings\r\nUpgrade: h2c\r\n"+
"HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA\r\n\r\n",
listener.Addr())
require.NoError(t, err)
response, err := http.ReadResponse(bufio.NewReader(conn), nil)
require.NoError(t, err)
defer response.Body.Close()
require.NotEqual(t, http.StatusSwitchingProtocols, response.StatusCode)
})
}
func httpProtocols(http1, unencryptedHTTP2 bool) *http.Protocols {
protocols := new(http.Protocols)
protocols.SetHTTP1(http1)
protocols.SetUnencryptedHTTP2(unencryptedHTTP2)
return protocols
}
func TestCheckRouteAuthByRequest(t *testing.T) {
rc := &RouteConfig{
Username: "alice",
Password: "secret",
}
t.Run("accepts nil route config", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
require.True(t, checkRouteAuthByRequest(req, nil))
})
t.Run("accepts route without credentials", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
require.True(t, checkRouteAuthByRequest(req, &RouteConfig{}))
})
t.Run("accepts authorization header", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.SetBasicAuth("alice", "secret")
require.True(t, checkRouteAuthByRequest(req, rc))
})
t.Run("accepts proxy authorization header", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
require.True(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects authorization fallback for proxy request", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "secret")
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects wrong proxy authorization even when authorization matches", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "secret")
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects when neither header matches", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "wrong")
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects proxy authorization on direct request", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
}
func TestGetRequestRouteUser(t *testing.T) {
t.Run("proxy request uses proxy authorization username", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Host = "target.example.com"
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "proxy-user", getRequestRouteUser(req))
})
t.Run("connect request keeps proxy authorization routing", func(t *testing.T) {
req := httptest.NewRequest("CONNECT", "http://target.example.com:443", nil)
req.Host = "target.example.com:443"
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "proxy-user", getRequestRouteUser(req))
})
t.Run("direct request uses authorization username", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Host = "example.com"
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "direct-user", getRequestRouteUser(req))
})
t.Run("proxy request does not fall back when proxy authorization is invalid", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Host = "target.example.com"
req.Header.Set("Proxy-Authorization", "Basic !!!")
req.SetBasicAuth("direct-user", "direct-pass")
require.Empty(t, getRequestRouteUser(req))
})
}