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