mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-02 08:26:59 +00:00
602 lines
16 KiB
Go
602 lines
16 KiB
Go
package splithttp_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/xtls/xray-core/common"
|
|
"github.com/xtls/xray-core/common/buf"
|
|
"github.com/xtls/xray-core/common/net"
|
|
"github.com/xtls/xray-core/common/protocol/tls/cert"
|
|
"github.com/xtls/xray-core/testing/servers/tcp"
|
|
"github.com/xtls/xray-core/testing/servers/udp"
|
|
"github.com/xtls/xray-core/transport/internet"
|
|
. "github.com/xtls/xray-core/transport/internet/splithttp"
|
|
"github.com/xtls/xray-core/transport/internet/stat"
|
|
"github.com/xtls/xray-core/transport/internet/tls"
|
|
)
|
|
|
|
func Test_ListenXHAndDial(t *testing.T) {
|
|
listenPort := tcp.PickPort()
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Path: "/sh",
|
|
},
|
|
}, func(conn stat.Connection) {
|
|
go func(c stat.Connection) {
|
|
defer c.Close()
|
|
|
|
var b [1024]byte
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
_, err := c.Read(b[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
common.Must2(c.Write([]byte("Response")))
|
|
}(conn)
|
|
})
|
|
common.Must(err)
|
|
ctx := context.Background()
|
|
streamSettings := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{Path: "sh"},
|
|
}
|
|
conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings)
|
|
|
|
common.Must(err)
|
|
_, err = conn.Write([]byte("Test connection 1"))
|
|
common.Must(err)
|
|
|
|
var b [1024]byte
|
|
fmt.Println("test2")
|
|
n, _ := io.ReadFull(conn, b[:])
|
|
fmt.Println("string is", n)
|
|
if string(b[:n]) != "Response" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
|
|
common.Must(conn.Close())
|
|
conn, err = Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings)
|
|
|
|
common.Must(err)
|
|
_, err = conn.Write([]byte("Test connection 2"))
|
|
common.Must(err)
|
|
n, _ = io.ReadFull(conn, b[:])
|
|
common.Must(err)
|
|
if string(b[:n]) != "Response" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
common.Must(conn.Close())
|
|
|
|
common.Must(listen.Close())
|
|
}
|
|
|
|
func TestDialWithRemoteAddr(t *testing.T) {
|
|
listenPort := tcp.PickPort()
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Path: "sh",
|
|
},
|
|
SocketSettings: &internet.SocketConfig{
|
|
TrustedXForwardedFor: []string{"X-Forwarded-For"},
|
|
},
|
|
}, func(conn stat.Connection) {
|
|
go func(c stat.Connection) {
|
|
defer c.Close()
|
|
|
|
var b [1024]byte
|
|
_, err := c.Read(b[:])
|
|
// common.Must(err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
_, err = c.Write([]byte(c.RemoteAddr().String()))
|
|
common.Must(err)
|
|
}(conn)
|
|
})
|
|
common.Must(err)
|
|
|
|
conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{Path: "sh", Headers: map[string]string{"X-Forwarded-For": "1.1.1.1"}},
|
|
})
|
|
|
|
common.Must(err)
|
|
_, err = conn.Write([]byte("Test connection 1"))
|
|
common.Must(err)
|
|
|
|
var b [1024]byte
|
|
n, _ := io.ReadFull(conn, b[:])
|
|
if string(b[:n]) != "1.1.1.1:0" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
|
|
common.Must(listen.Close())
|
|
}
|
|
|
|
func Test_ListenXHAndDial_TLS(t *testing.T) {
|
|
if runtime.GOARCH == "arm64" {
|
|
return
|
|
}
|
|
|
|
listenPort := tcp.PickPort()
|
|
|
|
start := time.Now()
|
|
|
|
ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost"))
|
|
|
|
streamSettings := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Path: "shs",
|
|
},
|
|
SecurityType: "tls",
|
|
SecuritySettings: &tls.Config{
|
|
Certificate: []*tls.Certificate{tls.ParseCertificate(ct)},
|
|
PinnedPeerCertSha256: [][]byte{ctHash[:]},
|
|
},
|
|
}
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) {
|
|
go func() {
|
|
defer conn.Close()
|
|
|
|
var b [1024]byte
|
|
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
_, err := conn.Read(b[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
common.Must2(conn.Write([]byte("Response")))
|
|
}()
|
|
})
|
|
common.Must(err)
|
|
defer listen.Close()
|
|
|
|
conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings)
|
|
common.Must(err)
|
|
|
|
_, err = conn.Write([]byte("Test connection 1"))
|
|
common.Must(err)
|
|
|
|
var b [1024]byte
|
|
n, _ := io.ReadFull(conn, b[:])
|
|
if string(b[:n]) != "Response" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
|
|
end := time.Now()
|
|
if !end.Before(start.Add(time.Second * 5)) {
|
|
t.Error("end: ", end, " start: ", start)
|
|
}
|
|
}
|
|
|
|
func Test_ListenXHAndDial_H2C(t *testing.T) {
|
|
if runtime.GOARCH == "arm64" {
|
|
return
|
|
}
|
|
|
|
listenPort := tcp.PickPort()
|
|
|
|
streamSettings := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Path: "shs",
|
|
},
|
|
}
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) {
|
|
go func() {
|
|
_ = conn.Close()
|
|
}()
|
|
})
|
|
common.Must(err)
|
|
defer listen.Close()
|
|
|
|
protocols := new(http.Protocols)
|
|
protocols.SetUnencryptedHTTP2(true)
|
|
client := http.Client{
|
|
Transport: &http.Transport{
|
|
Protocols: protocols,
|
|
},
|
|
}
|
|
|
|
resp, err := client.Get("http://" + net.LocalHostIP.String() + ":" + listenPort.String())
|
|
common.Must(err)
|
|
|
|
if resp.StatusCode != 404 {
|
|
t.Error("Expected 404 but got:", resp.StatusCode)
|
|
}
|
|
|
|
if resp.ProtoMajor != 2 {
|
|
t.Error("Expected h2 but got:", resp.ProtoMajor)
|
|
}
|
|
}
|
|
|
|
func Test_ListenXHAndDial_QUIC(t *testing.T) {
|
|
if runtime.GOARCH == "arm64" {
|
|
return
|
|
}
|
|
|
|
listenPort := udp.PickPort()
|
|
|
|
start := time.Now()
|
|
|
|
ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost"))
|
|
|
|
streamSettings := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Path: "shs",
|
|
},
|
|
SecurityType: "tls",
|
|
SecuritySettings: &tls.Config{
|
|
Certificate: []*tls.Certificate{tls.ParseCertificate(ct)},
|
|
PinnedPeerCertSha256: [][]byte{ctHash[:]},
|
|
NextProtocol: []string{"h3"},
|
|
},
|
|
}
|
|
|
|
serverClosed := false
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) {
|
|
go func() {
|
|
defer conn.Close()
|
|
|
|
b := buf.New()
|
|
defer b.Release()
|
|
|
|
for {
|
|
b.Clear()
|
|
if _, err := b.ReadFrom(conn); err != nil {
|
|
break
|
|
}
|
|
common.Must2(conn.Write(b.Bytes()))
|
|
}
|
|
|
|
serverClosed = true
|
|
}()
|
|
})
|
|
common.Must(err)
|
|
defer listen.Close()
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
conn, err := Dial(context.Background(), net.UDPDestination(net.DomainAddress("localhost"), listenPort), streamSettings)
|
|
common.Must(err)
|
|
|
|
const N = 1024
|
|
b1 := make([]byte, N)
|
|
common.Must2(rand.Read(b1))
|
|
b2 := buf.New()
|
|
|
|
common.Must2(conn.Write(b1))
|
|
|
|
b2.Clear()
|
|
common.Must2(b2.ReadFullFrom(conn, N))
|
|
if r := cmp.Diff(b2.Bytes(), b1); r != "" {
|
|
t.Error(r)
|
|
}
|
|
|
|
common.Must2(conn.Write(b1))
|
|
|
|
b2.Clear()
|
|
common.Must2(b2.ReadFullFrom(conn, N))
|
|
if r := cmp.Diff(b2.Bytes(), b1); r != "" {
|
|
t.Error(r)
|
|
}
|
|
|
|
conn.Close()
|
|
time.Sleep(100 * time.Millisecond)
|
|
if !serverClosed {
|
|
t.Error("server did not get closed")
|
|
}
|
|
|
|
end := time.Now()
|
|
if !end.Before(start.Add(time.Second * 5)) {
|
|
t.Error("end: ", end, " start: ", start)
|
|
}
|
|
}
|
|
|
|
func Test_ListenXHAndDial_Unix(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
tempSocket := tempDir + "/server.sock"
|
|
|
|
listen, err := ListenXH(context.Background(), net.DomainAddress(tempSocket), 0, &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Path: "/sh",
|
|
},
|
|
}, func(conn stat.Connection) {
|
|
go func(c stat.Connection) {
|
|
defer c.Close()
|
|
|
|
var b [1024]byte
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
_, err := c.Read(b[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
common.Must2(c.Write([]byte("Response")))
|
|
}(conn)
|
|
})
|
|
common.Must(err)
|
|
ctx := context.Background()
|
|
streamSettings := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Host: "example.com",
|
|
Path: "sh",
|
|
},
|
|
}
|
|
conn, err := Dial(ctx, net.UnixDestination(net.DomainAddress(tempSocket)), streamSettings)
|
|
|
|
common.Must(err)
|
|
_, err = conn.Write([]byte("Test connection 1"))
|
|
common.Must(err)
|
|
|
|
var b [1024]byte
|
|
fmt.Println("test2")
|
|
n, _ := io.ReadFull(conn, b[:])
|
|
fmt.Println("string is", n)
|
|
if string(b[:n]) != "Response" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
|
|
common.Must(conn.Close())
|
|
conn, err = Dial(ctx, net.UnixDestination(net.DomainAddress(tempSocket)), streamSettings)
|
|
|
|
common.Must(err)
|
|
_, err = conn.Write([]byte("Test connection 2"))
|
|
common.Must(err)
|
|
n, _ = io.ReadFull(conn, b[:])
|
|
common.Must(err)
|
|
if string(b[:n]) != "Response" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
common.Must(conn.Close())
|
|
|
|
common.Must(listen.Close())
|
|
}
|
|
|
|
func Test_queryString(t *testing.T) {
|
|
listenPort := tcp.PickPort()
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
// this querystring does not have any effect, but sometimes people blindly copy it from websocket config. make sure the outbound doesn't break
|
|
Path: "/sh?ed=2048",
|
|
},
|
|
}, func(conn stat.Connection) {
|
|
go func(c stat.Connection) {
|
|
defer c.Close()
|
|
|
|
var b [1024]byte
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
_, err := c.Read(b[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
common.Must2(c.Write([]byte("Response")))
|
|
}(conn)
|
|
})
|
|
common.Must(err)
|
|
ctx := context.Background()
|
|
streamSettings := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{Path: "sh?ed=2048"},
|
|
}
|
|
conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings)
|
|
|
|
common.Must(err)
|
|
_, err = conn.Write([]byte("Test connection 1"))
|
|
common.Must(err)
|
|
|
|
var b [1024]byte
|
|
fmt.Println("test2")
|
|
n, _ := io.ReadFull(conn, b[:])
|
|
fmt.Println("string is", n)
|
|
if string(b[:n]) != "Response" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
|
|
common.Must(conn.Close())
|
|
common.Must(listen.Close())
|
|
}
|
|
|
|
func Test_maxUpload(t *testing.T) {
|
|
listenPort := tcp.PickPort()
|
|
streamSettings := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{
|
|
Path: "/sh",
|
|
ScMaxEachPostBytes: &RangeConfig{
|
|
From: 10000,
|
|
To: 10000,
|
|
},
|
|
},
|
|
}
|
|
|
|
uploadReceived := make([]byte, 10001)
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) {
|
|
go func(c stat.Connection) {
|
|
defer c.Close()
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
io.ReadFull(c, uploadReceived)
|
|
|
|
common.Must2(c.Write([]byte("Response")))
|
|
}(conn)
|
|
})
|
|
common.Must(err)
|
|
ctx := context.Background()
|
|
|
|
conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings)
|
|
common.Must(err)
|
|
|
|
// send a slightly too large upload
|
|
upload := make([]byte, 10001)
|
|
rand.Read(upload)
|
|
_, err = conn.Write(upload)
|
|
common.Must(err)
|
|
|
|
var b [10240]byte
|
|
n, _ := io.ReadFull(conn, b[:])
|
|
fmt.Println("string is", n)
|
|
if string(b[:n]) != "Response" {
|
|
t.Error("response: ", string(b[:n]))
|
|
}
|
|
common.Must(conn.Close())
|
|
|
|
if !bytes.Equal(upload, uploadReceived) {
|
|
t.Error("incorrect upload", upload, uploadReceived)
|
|
}
|
|
|
|
common.Must(listen.Close())
|
|
}
|
|
|
|
func newPacketUpEchoServer(t *testing.T, serverCfg *Config) net.Port {
|
|
t.Helper()
|
|
port := tcp.PickPort()
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: serverCfg,
|
|
}, func(conn stat.Connection) {
|
|
go func(c stat.Connection) {
|
|
defer c.Close()
|
|
var b [5]byte
|
|
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
if _, err := io.ReadFull(c, b[:]); err != nil {
|
|
return
|
|
}
|
|
common.Must2(c.Write(append([]byte("echo:"), b[:]...)))
|
|
}(conn)
|
|
})
|
|
common.Must(err)
|
|
t.Cleanup(func() { listen.Close() })
|
|
return port
|
|
}
|
|
|
|
// uplinkEcho fails instead of hanging when the uplink never reaches the server.
|
|
func uplinkEcho(t *testing.T, mode, method, transport string) error {
|
|
t.Helper()
|
|
res := make(chan error, 1)
|
|
go func() { res <- uplinkEchoOnce(mode, method, transport) }()
|
|
select {
|
|
case err := <-res:
|
|
return err
|
|
case <-time.After(10 * time.Second):
|
|
return fmt.Errorf("timed out")
|
|
}
|
|
}
|
|
|
|
func uplinkEchoOnce(mode, method, transport string) error {
|
|
port := tcp.PickPort()
|
|
serverCfg := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{Path: "/sh", Mode: mode},
|
|
}
|
|
clientCfg := &internet.MemoryStreamConfig{
|
|
ProtocolName: "splithttp",
|
|
ProtocolSettings: &Config{Path: "/sh", Mode: mode, UplinkHTTPMethod: method},
|
|
}
|
|
if transport == "tls" {
|
|
ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost"))
|
|
sec := &tls.Config{Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, PinnedPeerCertSha256: [][]byte{ctHash[:]}}
|
|
serverCfg.SecurityType, serverCfg.SecuritySettings = "tls", sec
|
|
clientCfg.SecurityType, clientCfg.SecuritySettings = "tls", sec
|
|
}
|
|
listen, err := ListenXH(context.Background(), net.LocalHostIP, port, serverCfg, func(conn stat.Connection) {
|
|
go func(c stat.Connection) {
|
|
defer c.Close()
|
|
var b [5]byte
|
|
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
if _, err := io.ReadFull(c, b[:]); err != nil {
|
|
return
|
|
}
|
|
common.Must2(c.Write(append([]byte("echo:"), b[:]...)))
|
|
}(conn)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer listen.Close()
|
|
|
|
conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), port), clientCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
if _, err = conn.Write([]byte("hello")); err != nil {
|
|
return err
|
|
}
|
|
var b [1024]byte
|
|
conn.SetReadDeadline(time.Now().Add(4 * time.Second))
|
|
n, err := io.ReadFull(conn, b[:len("echo:hello")])
|
|
if err != nil || string(b[:n]) != "echo:hello" {
|
|
return fmt.Errorf("got %q: %v", b[:n], err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// OPTIONS must carry uplink data exactly like POST does, in every mode and over every transport.
|
|
func Test_UplinkHTTPMethodOptions(t *testing.T) {
|
|
for _, transport := range []string{"plain", "tls"} {
|
|
for _, mode := range []string{"packet-up", "stream-up", "stream-one", "auto"} {
|
|
t.Run(transport+"/"+mode, func(t *testing.T) {
|
|
if transport == "tls" && runtime.GOARCH == "arm64" {
|
|
t.Skip("TLS tests are skipped on arm64, like the other TLS tests in this package")
|
|
}
|
|
if err := uplinkEcho(t, mode, "POST", transport); err != nil {
|
|
t.Skipf("POST baseline does not work in this setup: %v", err)
|
|
}
|
|
for _, method := range []string{"PUT", "OPTIONS"} {
|
|
if err := uplinkEcho(t, mode, method, transport); err != nil {
|
|
t.Errorf("%s: %v", method, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func Test_CORSPreflightStillAnswered(t *testing.T) {
|
|
port := newPacketUpEchoServer(t, &Config{Path: "/sh", Mode: "packet-up"})
|
|
req, _ := http.NewRequest("OPTIONS", fmt.Sprintf("http://127.0.0.1:%d/sh/", port), nil)
|
|
req.Header.Set("Origin", "https://example.com")
|
|
req.Header.Set("Access-Control-Request-Method", "POST")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
common.Must(err)
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("preflight status = %d", resp.StatusCode)
|
|
}
|
|
if got := resp.Header.Get("Access-Control-Allow-Methods"); got != "POST" {
|
|
t.Errorf("Allow-Methods = %q", got)
|
|
}
|
|
}
|
|
|
|
func Test_ServerMaxHeaderBytes(t *testing.T) {
|
|
port := newPacketUpEchoServer(t, &Config{Path: "/sh", Mode: "packet-up", ServerMaxHeaderBytes: 2048})
|
|
big := bytes.Repeat([]byte("a"), 8192)
|
|
req, _ := http.NewRequest("GET", fmt.Sprintf("http://127.0.0.1:%d/sh/", port), nil)
|
|
req.Header.Set("X-Big", string(big))
|
|
resp, err := http.DefaultClient.Do(req)
|
|
common.Must(err)
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusRequestHeaderFieldsTooLarge {
|
|
t.Fatalf("oversized header status = %d, want 431", resp.StatusCode)
|
|
}
|
|
}
|