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
+199 -13
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: