XHTTP server: Accept OPTIONS as uplinkHTTPMethod (#7290)

https://github.com/XTLS/Xray-core/pull/7290#issuecomment-6505845186
This commit is contained in:
konstpic
2026-10-31 08:43:57 +00:00
committed by RPRX
parent 9b8190f52c
commit e7a69fa85a
2 changed files with 137 additions and 1 deletions
+3 -1
View File
@@ -126,7 +126,9 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
h.config.ApplyXPaddingToResponse(writer, config)
if request.Method == "OPTIONS" {
// A real CORS preflight always carries Access-Control-Request-Method. An OPTIONS request without it is an
// uplink request (uplinkHTTPMethod: "OPTIONS") and must go through the normal handling below.
if request.Method == "OPTIONS" && request.Header.Get("Access-Control-Request-Method") != "" {
writer.WriteHeader(http.StatusOK)
return
}
@@ -465,3 +465,137 @@ func Test_maxUpload(t *testing.T) {
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)
}
}