Update xhttp

This commit is contained in:
Shtorm
2026-02-22 14:48:52 +03:00
parent 9e4eb52a82
commit a3aaf2cb92
9 changed files with 863 additions and 104 deletions

View File

@@ -3,14 +3,16 @@ package xhttp
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
"strings"
"sync"
"github.com/sagernet/sing-box/common/xray"
common "github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing-box/option"
)
@@ -19,11 +21,11 @@ import (
type DialerClient interface {
IsClosed() bool
// ctx, url, body, uploadOnly
OpenStream(context.Context, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
// ctx, url, sessionId, body, uploadOnly
OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
// ctx, url, body, contentLength
PostPacket(context.Context, string, io.Reader, int64) error
// ctx, url, sessionId, seqStr, body, contentLength
PostPacket(context.Context, string, string, string, io.Reader, int64) error
}
// implements xhttp.DialerClient in terms of direct network connections
@@ -41,7 +43,7 @@ func (c *DefaultDialerClient) IsClosed() bool {
return c.closed
}
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
// this is done when the TCP/UDP connection to the server was established,
// and we can unblock the Dial function and print correct net addresses in
// logs
@@ -55,11 +57,31 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
})
method := "GET" // stream-down
if body != nil {
method = "POST" // stream-up/one
method = c.options.GetNormalizedUplinkHTTPMethod() // stream-up/one
}
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
req.Header = c.options.GetRequestHeader(url)
if method == "POST" && !c.options.NoGRPCHeader {
req.Header = c.options.GetRequestHeader()
length := int(c.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if c.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.options.XPaddingPlacement,
Key: c.options.XPaddingKey,
Header: c.options.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
ApplyXPaddingToRequest(req, config)
ApplyMetaToRequest(c.options, req, sessionId, "")
if method == c.options.GetNormalizedUplinkHTTPMethod() && !c.options.NoGRPCHeader {
req.Header.Set("Content-Type", "application/grpc")
}
wrc = &WaitReadCloser{Wait: make(chan struct{})}
@@ -85,13 +107,76 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
return
}
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error {
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), "POST", url, body)
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
var encodedData string
dataPlacement := c.options.GetNormalizedUplinkDataPlacement()
if dataPlacement != option.PlacementBody {
data, err := io.ReadAll(body)
if err != nil {
return err
}
encodedData = base64.RawURLEncoding.EncodeToString(data)
body = nil
contentLength = 0
}
method := c.options.GetNormalizedUplinkHTTPMethod()
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
if err != nil {
return err
}
req.ContentLength = contentLength
req.Header = c.options.GetRequestHeader(url)
req.Header = c.options.GetRequestHeader()
if dataPlacement != option.PlacementBody {
key := c.options.UplinkDataKey
chunkSize := int(c.options.UplinkChunkSize)
switch dataPlacement {
case option.PlacementHeader:
for i := 0; i < len(encodedData); i += chunkSize {
end := i + chunkSize
if end > len(encodedData) {
end = len(encodedData)
}
chunk := encodedData[i:end]
headerKey := fmt.Sprintf("%s-%d", key, i/chunkSize)
req.Header.Set(headerKey, chunk)
}
req.Header.Set(key+"-Length", fmt.Sprintf("%d", len(encodedData)))
req.Header.Set(key+"-Upstream", "1")
case option.PlacementCookie:
for i := 0; i < len(encodedData); i += chunkSize {
end := i + chunkSize
if end > len(encodedData) {
end = len(encodedData)
}
chunk := encodedData[i:end]
cookieName := fmt.Sprintf("%s_%d", key, i/chunkSize)
req.AddCookie(&http.Cookie{Name: cookieName, Value: chunk})
}
req.AddCookie(&http.Cookie{Name: key + "_upstream", Value: "1"})
}
}
length := int(c.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if c.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.options.XPaddingPlacement,
Key: c.options.XPaddingKey,
Header: c.options.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
ApplyXPaddingToRequest(req, config)
ApplyMetaToRequest(c.options, req, sessionId, seqStr)
if c.httpVersion != "1.1" {
resp, err := c.client.Do(req)
if err != nil {
@@ -150,7 +235,6 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body i
}
c.uploadRawPool.Put(uploadConn)
}
return nil
}
@@ -190,3 +274,45 @@ func (w *WaitReadCloser) Close() error {
close(w.Wait)
return nil
}
func ApplyMetaToRequest(options *option.V2RayXHTTPBaseOptions, req *http.Request, sessionId string, seqStr string) {
sessionPlacement := options.GetNormalizedSessionPlacement()
seqPlacement := options.GetNormalizedSeqPlacement()
sessionKey := options.GetNormalizedSessionKey()
seqKey := options.GetNormalizedSeqKey()
if sessionId != "" {
switch sessionPlacement {
case option.PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, sessionId)
case option.PlacementQuery:
q := req.URL.Query()
q.Set(sessionKey, sessionId)
req.URL.RawQuery = q.Encode()
case option.PlacementHeader:
req.Header.Set(sessionKey, sessionId)
case option.PlacementCookie:
req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId})
}
}
if seqStr != "" {
switch seqPlacement {
case option.PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, seqStr)
case option.PlacementQuery:
q := req.URL.Query()
q.Set(seqKey, seqStr)
req.URL.RawQuery = q.Encode()
case option.PlacementHeader:
req.Header.Set(seqKey, seqStr)
case option.PlacementCookie:
req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr})
}
}
}
func appendToPath(path, value string) string {
if strings.HasSuffix(path, "/") {
return path + value
}
return path + "/" + value
}