mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-05-08 14:13:22 +00:00
XHTTP transport: Bugfixes for obfuscations (#5720)
https://github.com/XTLS/Xray-core/pull/5720#issuecomment-4016290343
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,4 +7,5 @@ const (
|
||||
PlacementQuery = "query"
|
||||
PlacementPath = "path"
|
||||
PlacementBody = "body"
|
||||
PlacementAuto = "auto"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -48,5 +48,6 @@ message Config {
|
||||
string seqKey = 23;
|
||||
string uplinkDataPlacement = 24;
|
||||
string uplinkDataKey = 25;
|
||||
uint32 uplinkChunkSize = 26;
|
||||
RangeConfig uplinkChunkSize = 26;
|
||||
int32 serverMaxHeaderBytes = 27;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 "", ""
|
||||
|
||||
Reference in New Issue
Block a user