XHTTP transport: Bugfixes for obfuscations (#5720)

https://github.com/XTLS/Xray-core/pull/5720#issuecomment-4016290343
This commit is contained in:
26X23
2026-03-07 12:34:41 +00:00
committed by GitHub
parent eec280262d
commit 0ac13bd910
12 changed files with 558 additions and 413 deletions

View File

@@ -3,6 +3,7 @@ package splithttp
import (
"context"
"io"
"net/http"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
@@ -19,35 +20,19 @@ func (c *BrowserDialerClient) IsClosed() bool {
panic("not implemented yet")
}
func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, _ string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) {
func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) {
if body != nil {
return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet")
}
header := c.transportConfig.GetRequestHeader()
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
if c.transportConfig.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.transportConfig.XPaddingPlacement,
Key: c.transportConfig.XPaddingKey,
Header: c.transportConfig.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, nil, nil, err
}
c.transportConfig.ApplyXPaddingToHeader(header, config)
c.transportConfig.FillStreamRequest(request, sessionId, "")
conn, err := browser_dialer.DialGet(url, header)
conn, err := browser_dialer.DialGet(request.URL.String(), request.Header, request.Cookies())
dummyAddr := &net.IPAddr{}
if err != nil {
return nil, dummyAddr, dummyAddr, err
@@ -56,36 +41,28 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, _ stri
return websocket.NewConnection(conn, dummyAddr, nil, 0), conn.RemoteAddr(), conn.LocalAddr(), nil
}
func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, _ string, _ string, body io.Reader, contentLength int64) error {
bytes, err := io.ReadAll(body)
func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
method := c.transportConfig.GetNormalizedUplinkHTTPMethod()
request, err := http.NewRequest(method, url, body)
if err != nil {
return err
}
header := c.transportConfig.GetRequestHeader()
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
request.ContentLength = contentLength
err = c.transportConfig.FillPacketRequest(request, sessionId, seqStr)
if err != nil {
return err
}
if c.transportConfig.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.transportConfig.XPaddingPlacement,
Key: c.transportConfig.XPaddingKey,
Header: c.transportConfig.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
var bytes []byte
if (request.Body != nil) {
bytes, err = io.ReadAll(request.Body)
if err != nil {
return err
}
}
c.transportConfig.ApplyXPaddingToHeader(header, config)
err = browser_dialer.DialPost(url, header, bytes)
err = browser_dialer.DialPacket(method, request.URL.String(), request.Header, request.Cookies(), bytes)
if err != nil {
return err
}

View File

@@ -3,7 +3,6 @@ package splithttp
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
@@ -60,33 +59,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
method = c.transportConfig.GetNormalizedUplinkHTTPMethod() // stream-up/one
}
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
req.Header = c.transportConfig.GetRequestHeader()
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
if c.transportConfig.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.transportConfig.XPaddingPlacement,
Key: c.transportConfig.XPaddingKey,
Header: c.transportConfig.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
c.transportConfig.ApplyXPaddingToRequest(req, config)
c.transportConfig.ApplyMetaToRequest(req, sessionId, "")
if method == c.transportConfig.GetNormalizedUplinkHTTPMethod() && !c.transportConfig.NoGRPCHeader {
req.Header.Set("Content-Type", "application/grpc")
}
c.transportConfig.FillStreamRequest(req, sessionId, "")
wrc = &WaitReadCloser{Wait: make(chan struct{})}
go func() {
@@ -117,82 +90,13 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
}
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
var encodedData string
dataPlacement := c.transportConfig.GetNormalizedUplinkDataPlacement()
if dataPlacement != PlacementBody {
data, err := io.ReadAll(body)
if err != nil {
return err
}
encodedData = base64.RawURLEncoding.EncodeToString(data)
body = nil
contentLength = 0
}
method := c.transportConfig.GetNormalizedUplinkHTTPMethod()
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
if err != nil {
return err
}
req.ContentLength = contentLength
req.Header = c.transportConfig.GetRequestHeader()
if dataPlacement != PlacementBody {
key := c.transportConfig.UplinkDataKey
chunkSize := int(c.transportConfig.UplinkChunkSize)
switch dataPlacement {
case 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 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.transportConfig.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
if c.transportConfig.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.transportConfig.XPaddingPlacement,
Key: c.transportConfig.XPaddingKey,
Header: c.transportConfig.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
c.transportConfig.ApplyXPaddingToRequest(req, config)
c.transportConfig.ApplyMetaToRequest(req, sessionId, seqStr)
c.transportConfig.FillPacketRequest(req, sessionId, seqStr)
if c.httpVersion != "1.1" {
resp, err := c.client.Do(req)

View File

@@ -7,4 +7,5 @@ const (
PlacementQuery = "query"
PlacementPath = "path"
PlacementBody = "body"
PlacementAuto = "auto"
)

View File

@@ -1,6 +1,9 @@
package splithttp
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"
@@ -54,11 +57,72 @@ func (c *Config) GetRequestHeader() http.Header {
return header
}
func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
func (c *Config) GetRequestHeaderWithPayload(payload []byte) http.Header {
header := c.GetRequestHeader()
key := c.UplinkDataKey
encodedData := base64.RawURLEncoding.EncodeToString(payload)
for i := 0; len(encodedData) > 0; i++ {
chunkSize := min(int(c.GetNormalizedUplinkChunkSize().rand()), len(encodedData))
chunk := encodedData[:chunkSize]
encodedData = encodedData[chunkSize:]
headerKey := fmt.Sprintf("%s-%d", key, i)
header.Set(headerKey, chunk)
}
return header
}
func (c *Config) GetRequestCookiesWithPayload(payload []byte) []*http.Cookie {
cookies := []*http.Cookie{}
key := c.UplinkDataKey
encodedData := base64.RawURLEncoding.EncodeToString(payload)
for i := 0; len(encodedData) > 0; i++ {
chunkSize := min(int(c.GetNormalizedUplinkChunkSize().rand()), len(encodedData))
chunk := encodedData[:chunkSize]
encodedData = encodedData[chunkSize:]
cookieName := fmt.Sprintf("%s_%d", key, i)
cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk})
}
return cookies
}
func (c *Config) WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header) {
// CORS headers for the browser dialer
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Access-Control-Allow-Methods", "*")
// writer.Header().Set("X-Version", core.Version())
if origin := requestHeader.Get("Origin"); origin == "" {
writer.Header().Set("Access-Control-Allow-Origin", "*")
} else {
// Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
writer.Header().Set("Access-Control-Allow-Origin", origin)
}
if c.GetNormalizedSessionPlacement() == PlacementCookie ||
c.GetNormalizedSeqPlacement() == PlacementCookie ||
c.XPaddingPlacement == PlacementCookie ||
c.GetNormalizedUplinkDataPlacement() == PlacementCookie {
writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
if requestMethod == "OPTIONS" {
requestedMethod := requestHeader.Get("Access-Control-Request-Method")
if requestedMethod != "" {
writer.Header().Set("Access-Control-Allow-Methods", requestedMethod)
} else {
writer.Header().Set("Access-Control-Allow-Methods", "*")
}
requestedHeaders := requestHeader.Get("Access-Control-Request-Headers")
if requestedHeaders == "" {
writer.Header().Set("Access-Control-Allow-Headers", "*")
} else {
writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders)
}
}
}
func (c *Config) GetNormalizedUplinkHTTPMethod() string {
@@ -110,6 +174,40 @@ func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig {
return *c.ScStreamUpServerSecs
}
func (c *Config) GetNormalizedUplinkChunkSize() RangeConfig {
if c.UplinkChunkSize == nil || c.UplinkChunkSize.To == 0 {
switch c.UplinkDataPlacement {
case PlacementCookie:
return RangeConfig{
From: 2 * 1024, // 2 KiB
To: 3 * 1024, // 3 KiB
}
case PlacementHeader:
return RangeConfig{
From: 3 * 1000, // 3 KB
To: 4 * 1000, // 4 KB
}
default:
return c.GetNormalizedScMaxEachPostBytes()
}
} else if c.UplinkChunkSize.From < 64 {
return RangeConfig{
From: 64,
To: max(64, c.UplinkChunkSize.To),
}
}
return *c.UplinkChunkSize
}
func (c *Config) GetNormalizedServerMaxHeaderBytes() int {
if c.ServerMaxHeaderBytes <= 0 {
return 8192
} else {
return int(c.ServerMaxHeaderBytes)
}
}
func (c *Config) GetNormalizedSessionPlacement() string {
if c.SessionPlacement == "" {
return PlacementPath
@@ -196,24 +294,107 @@ func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr
}
}
func (c *Config) FillStreamRequest(request *http.Request, sessionId string, seqStr string) {
request.Header = c.GetRequestHeader()
length := int(c.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
if c.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.XPaddingPlacement,
Key: c.XPaddingKey,
Header: c.XPaddingHeader,
RawURL: request.URL.String(),
}
config.Method = PaddingMethod(c.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: request.URL.String(),
}
}
c.ApplyXPaddingToRequest(request, config)
c.ApplyMetaToRequest(request, sessionId, "")
if request.Body != nil && !c.NoGRPCHeader { // stream-up/one
request.Header.Set("Content-Type", "application/grpc")
}
}
func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string) error {
dataPlacement := c.GetNormalizedUplinkDataPlacement()
if dataPlacement == PlacementBody || dataPlacement == PlacementAuto {
request.Header = c.GetRequestHeader()
} else {
var data []byte
var err error
if request.Body != nil {
data, err = io.ReadAll(request.Body)
if err != nil {
return err
}
}
request.Body = nil
request.ContentLength = 0
switch dataPlacement {
case PlacementHeader:
request.Header = c.GetRequestHeaderWithPayload(data)
case PlacementCookie:
request.Header = c.GetRequestHeader()
for _, cookie := range c.GetRequestCookiesWithPayload(data) {
request.AddCookie(cookie)
}
}
}
length := int(c.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
if c.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.XPaddingPlacement,
Key: c.XPaddingKey,
Header: c.XPaddingHeader,
RawURL: request.URL.String(),
}
config.Method = PaddingMethod(c.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: request.URL.String(),
}
}
c.ApplyXPaddingToRequest(request, config)
c.ApplyMetaToRequest(request, sessionId, seqStr)
return nil
}
func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) {
sessionPlacement := c.GetNormalizedSessionPlacement()
seqPlacement := c.GetNormalizedSeqPlacement()
sessionKey := c.GetNormalizedSessionKey()
seqKey := c.GetNormalizedSeqKey()
if sessionPlacement == PlacementPath && seqPlacement == PlacementPath {
subpath := strings.Split(req.URL.Path[len(path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
if len(subpath) > 1 {
seqStr = subpath[1]
}
return sessionId, seqStr
var subpath []string
pathPart := 0
if sessionPlacement == PlacementPath || seqPlacement == PlacementPath {
subpath = strings.Split(req.URL.Path[len(path):], "/")
}
switch sessionPlacement {
case PlacementPath:
if len(subpath) > pathPart {
sessionId = subpath[pathPart]
pathPart += 1
}
case PlacementQuery:
sessionId = req.URL.Query().Get(sessionKey)
case PlacementHeader:
@@ -225,6 +406,11 @@ func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (session
}
switch seqPlacement {
case PlacementPath:
if len(subpath) > pathPart {
seqStr = subpath[pathPart]
pathPart += 1
}
case PlacementQuery:
seqStr = req.URL.Query().Get(seqKey)
case PlacementHeader:

View File

@@ -185,7 +185,8 @@ type Config struct {
SeqKey string `protobuf:"bytes,23,opt,name=seqKey,proto3" json:"seqKey,omitempty"`
UplinkDataPlacement string `protobuf:"bytes,24,opt,name=uplinkDataPlacement,proto3" json:"uplinkDataPlacement,omitempty"`
UplinkDataKey string `protobuf:"bytes,25,opt,name=uplinkDataKey,proto3" json:"uplinkDataKey,omitempty"`
UplinkChunkSize uint32 `protobuf:"varint,26,opt,name=uplinkChunkSize,proto3" json:"uplinkChunkSize,omitempty"`
UplinkChunkSize *RangeConfig `protobuf:"bytes,26,opt,name=uplinkChunkSize,proto3" json:"uplinkChunkSize,omitempty"`
ServerMaxHeaderBytes int32 `protobuf:"varint,27,opt,name=serverMaxHeaderBytes,proto3" json:"serverMaxHeaderBytes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -395,10 +396,17 @@ func (x *Config) GetUplinkDataKey() string {
return ""
}
func (x *Config) GetUplinkChunkSize() uint32 {
func (x *Config) GetUplinkChunkSize() *RangeConfig {
if x != nil {
return x.UplinkChunkSize
}
return nil
}
func (x *Config) GetServerMaxHeaderBytes() int32 {
if x != nil {
return x.ServerMaxHeaderBytes
}
return 0
}
@@ -417,8 +425,7 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" +
"\x0ecMaxReuseTimes\x18\x03 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0ecMaxReuseTimes\x12Z\n" +
"\x10hMaxRequestTimes\x18\x04 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxRequestTimes\x12Z\n" +
"\x10hMaxReusableSecs\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxReusableSecs\x12*\n" +
"\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xde\n" +
"\n" +
"\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xc2\v\n" +
"\x06Config\x12\x12\n" +
"\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" +
@@ -447,8 +454,9 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" +
"\fseqPlacement\x18\x16 \x01(\tR\fseqPlacement\x12\x16\n" +
"\x06seqKey\x18\x17 \x01(\tR\x06seqKey\x120\n" +
"\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" +
"\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12(\n" +
"\x0fuplinkChunkSize\x18\x1a \x01(\rR\x0fuplinkChunkSize\x1a:\n" +
"\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12X\n" +
"\x0fuplinkChunkSize\x18\x1a \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0fuplinkChunkSize\x122\n" +
"\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x1a:\n" +
"\fHeadersEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\n" +
@@ -487,11 +495,12 @@ var file_transport_internet_splithttp_config_proto_depIdxs = []int32{
0, // 9: xray.transport.internet.splithttp.Config.scStreamUpServerSecs:type_name -> xray.transport.internet.splithttp.RangeConfig
1, // 10: xray.transport.internet.splithttp.Config.xmux:type_name -> xray.transport.internet.splithttp.XmuxConfig
4, // 11: xray.transport.internet.splithttp.Config.downloadSettings:type_name -> xray.transport.internet.StreamConfig
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
0, // 12: xray.transport.internet.splithttp.Config.uplinkChunkSize:type_name -> xray.transport.internet.splithttp.RangeConfig
13, // [13:13] is the sub-list for method output_type
13, // [13:13] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
}
func init() { file_transport_internet_splithttp_config_proto_init() }

View File

@@ -48,5 +48,6 @@ message Config {
string seqKey = 23;
string uplinkDataPlacement = 24;
string uplinkDataKey = 25;
uint32 uplinkChunkSize = 26;
RangeConfig uplinkChunkSize = 26;
int32 serverMaxHeaderBytes = 27;
}

View File

@@ -396,8 +396,8 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
scMaxEachPostBytes := transportConfiguration.GetNormalizedScMaxEachPostBytes()
scMinPostsIntervalMs := transportConfiguration.GetNormalizedScMinPostsIntervalMs()
if scMaxEachPostBytes.From <= buf.Size {
panic("`scMaxEachPostBytes` should be bigger than " + strconv.Itoa(buf.Size))
if scMaxEachPostBytes.From <= 0 {
panic("`scMaxEachPostBytes` should be bigger than 0")
}
maxUploadSize := scMaxEachPostBytes.rand()
@@ -405,7 +405,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
// code relies on this behavior. Subtract 1 so that together with
// uploadWriter wrapper, exact size limits can be enforced
// uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize - 1))
uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize - buf.Size))
uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(max(0, maxUploadSize - buf.Size)))
conn.writer = uploadWriter{
uploadPipeWriter,
@@ -417,57 +417,64 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
var lastWrite time.Time
for {
wroteRequest := done.New()
ctx := httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
WroteRequest: func(httptrace.WroteRequestInfo) {
wroteRequest.Close()
},
})
// this intentionally makes a shallow-copy of the struct so we
// can reassign Path (potentially concurrently)
url := requestURL
seqStr := strconv.FormatInt(seq, 10)
seq += 1
if scMinPostsIntervalMs.From > 0 {
time.Sleep(time.Duration(scMinPostsIntervalMs.rand())*time.Millisecond - time.Since(lastWrite))
}
// by offloading the uploads into a buffered pipe, multiple conn.Write
// calls get automatically batched together into larger POST requests.
// without batching, bandwidth is extremely limited.
chunk, err := uploadPipeReader.ReadMultiBuffer()
remainder, err := uploadPipeReader.ReadMultiBuffer()
if err != nil {
break
}
lastWrite = time.Now()
if xmuxClient != nil && (xmuxClient.LeftRequests.Add(-1) <= 0 ||
(xmuxClient.UnreusableAt != time.Time{} && lastWrite.After(xmuxClient.UnreusableAt))) {
httpClient, xmuxClient = getHTTPClient(ctx, dest, streamSettings)
}
go func() {
err := httpClient.PostPacket(
ctx,
url.String(),
sessionId,
seqStr,
&buf.MultiBufferContainer{MultiBuffer: chunk},
int64(chunk.Len()),
)
wroteRequest.Close()
if err != nil {
errors.LogInfoInner(ctx, err, "failed to send upload")
uploadPipeReader.Interrupt()
doSplit := atomic.Bool{}
for doSplit.Store(true); doSplit.Load(); {
var chunk buf.MultiBuffer
remainder, chunk = buf.SplitSize(remainder, maxUploadSize)
if chunk.IsEmpty() {
break
}
}()
if _, ok := httpClient.(*DefaultDialerClient); ok {
<-wroteRequest.Wait()
wroteRequest := done.New()
ctx := httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
WroteRequest: func(httptrace.WroteRequestInfo) {
wroteRequest.Close()
},
})
seqStr := strconv.FormatInt(seq, 10)
seq += 1
if scMinPostsIntervalMs.From > 0 {
time.Sleep(time.Duration(scMinPostsIntervalMs.rand())*time.Millisecond - time.Since(lastWrite))
}
lastWrite = time.Now()
if xmuxClient != nil && (xmuxClient.LeftRequests.Add(-1) <= 0 ||
(xmuxClient.UnreusableAt != time.Time{} && lastWrite.After(xmuxClient.UnreusableAt))) {
httpClient, xmuxClient = getHTTPClient(ctx, dest, streamSettings)
}
go func() {
err := httpClient.PostPacket(
ctx,
requestURL.String(),
sessionId,
seqStr,
&buf.MultiBufferContainer{MultiBuffer: chunk},
int64(chunk.Len()),
)
wroteRequest.Close()
if err != nil {
errors.LogInfoInner(ctx, err, "failed to send upload")
uploadPipeReader.Interrupt()
doSplit.Store(false)
}
}()
if _, ok := httpClient.(*DefaultDialerClient); ok {
<-wroteRequest.Wait()
}
}
}
}()

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strconv"
"strings"
"sync"
@@ -100,7 +101,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return
}
h.config.WriteResponseHeader(writer)
h.config.WriteResponseHeader(writer, request.Method, request.Header)
length := int(h.config.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
@@ -118,7 +119,12 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
}
}
h.config.ApplyXPaddingToHeader(writer.Header(), config)
h.config.ApplyXPaddingToResponse(writer, config)
if request.Method == "OPTIONS" {
writer.WriteHeader(http.StatusOK)
return
}
/*
clientVer := []int{0, 0, 0}
@@ -183,27 +189,17 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
currentSession = h.upsertSession(sessionId)
}
scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To)
uplinkHTTPMethod := h.config.GetNormalizedUplinkHTTPMethod()
isUplinkRequest := false
if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
switch request.Method {
case "GET":
isUplinkRequest = seqStr != ""
default:
isUplinkRequest = true
}
uplinkDataPlacement := h.config.GetNormalizedUplinkDataPlacement()
uplinkDataKey := h.config.UplinkDataKey
switch uplinkDataPlacement {
case PlacementHeader:
if request.Header.Get(uplinkDataKey+"-Upstream") == "1" {
isUplinkRequest = true
}
case PlacementCookie:
if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" {
isUplinkRequest = true
}
}
if isUplinkRequest && sessionId != "" { // stream-up, packet-up
if seqStr == "" {
if h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-up" {
@@ -254,75 +250,64 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return
}
var payload []byte
if uplinkDataPlacement != PlacementBody {
var encodedStr string
switch uplinkDataPlacement {
case PlacementHeader:
dataLenStr := request.Header.Get(uplinkDataKey + "-Length")
if dataLenStr != "" {
dataLen, _ := strconv.Atoi(dataLenStr)
var chunks []string
i := 0
for {
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
chunks = append(chunks, chunk)
i++
}
encodedStr = strings.Join(chunks, "")
if len(encodedStr) != dataLen {
encodedStr = ""
}
dataPlacement := h.config.GetNormalizedUplinkDataPlacement()
var headerPayload []byte
if dataPlacement == PlacementAuto || dataPlacement == PlacementHeader {
var headerPayloadChunks [] string
for i := 0; true; i++ {
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
case PlacementCookie:
var chunks []string
i := 0
headerPayloadChunks = append(headerPayloadChunks, chunk)
}
headerPayloadEncoded := strings.Join(headerPayloadChunks, "")
headerPayload, err = base64.RawURLEncoding.DecodeString(headerPayloadEncoded)
if err != nil {
errors.LogInfo(context.Background(), "Invalid base64 in header's payload: ", err.Error())
writer.WriteHeader(http.StatusBadRequest)
return
}
}
for {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := request.Cookie(cookieName); c != nil {
chunks = append(chunks, c.Value)
i++
} else {
break
}
}
if len(chunks) > 0 {
encodedStr = strings.Join(chunks, "")
var cookiePayload []byte
if dataPlacement == PlacementAuto || dataPlacement == PlacementCookie {
var cookiePayloadChunks []string
for i := 0; true; i++ {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := request.Cookie(cookieName); c != nil {
cookiePayloadChunks = append(cookiePayloadChunks, c.Value)
} else {
break
}
}
cookiePayloadEncoded := strings.Join(cookiePayloadChunks, "")
cookiePayload, err = base64.RawURLEncoding.DecodeString(cookiePayloadEncoded)
if err != nil {
errors.LogInfo(context.Background(), "Invalid base64 in cookies' payload: ", err.Error())
writer.WriteHeader(http.StatusBadRequest)
return
}
}
if encodedStr != "" {
payload, err = base64.RawURLEncoding.DecodeString(encodedStr)
} else {
errors.LogInfoInner(context.Background(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement)
var bodyPayload []byte
if dataPlacement == PlacementAuto || dataPlacement == PlacementBody {
bodyPayload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to upload (ReadAll)")
writer.WriteHeader(http.StatusInternalServerError)
return
}
} else {
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
}
payload := slices.Concat(headerPayload, cookiePayload, bodyPayload)
if len(payload) > scMaxEachPostBytes {
errors.LogInfo(context.Background(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.")
writer.WriteHeader(http.StatusRequestEntityTooLarge)
return
}
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to upload (ReadAll)")
writer.WriteHeader(http.StatusInternalServerError)
return
}
seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)")
@@ -341,6 +326,11 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return
}
if len(bodyPayload) == 0 {
// Methods without a body are usually cached by default.
writer.Header().Set("Cache-Control", "no-store")
}
writer.WriteHeader(http.StatusOK)
} else if request.Method == "GET" || sessionId == "" { // stream-down, stream-one
if sessionId != "" {
@@ -519,7 +509,7 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet
l.server = http.Server{
Handler: handler,
ReadHeaderTimeout: time.Second * 4,
MaxHeaderBytes: 8192,
MaxHeaderBytes: l.config.GetNormalizedServerMaxHeaderBytes(),
Protocols: protocols,
}
go func() {

View File

@@ -156,6 +156,17 @@ func ApplyPaddingToCookie(req *http.Request, name, value string) {
})
}
func ApplyPaddingToResponseCookie(writer http.ResponseWriter, name, value string) {
if name == "" || value == "" {
return
}
http.SetCookie(writer, &http.Cookie{
Name: name,
Value: value,
Path: "/",
})
}
func ApplyPaddingToQuery(u *url.URL, key, value string) {
if u == nil || key == "" || value == "" {
return
@@ -221,6 +232,22 @@ func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig
}
}
func (c *Config) ApplyXPaddingToResponse(writer http.ResponseWriter, config XPaddingConfig) {
placement := config.Placement.Placement
if placement == PlacementHeader || placement == PlacementQueryInHeader {
c.ApplyXPaddingToHeader(writer.Header(), config)
return
}
paddingValue := GeneratePadding(config.Method, config.Length)
switch placement {
case PlacementCookie:
ApplyPaddingToResponseCookie(writer, config.Placement.Key, paddingValue)
}
}
func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) {
if req == nil {
return "", ""