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
+12 -20
View File
@@ -230,13 +230,14 @@ type SplitHTTPConfig struct {
SeqKey string `json:"seqKey"` SeqKey string `json:"seqKey"`
UplinkDataPlacement string `json:"uplinkDataPlacement"` UplinkDataPlacement string `json:"uplinkDataPlacement"`
UplinkDataKey string `json:"uplinkDataKey"` UplinkDataKey string `json:"uplinkDataKey"`
UplinkChunkSize uint32 `json:"uplinkChunkSize"` UplinkChunkSize Int32Range `json:"uplinkChunkSize"`
NoGRPCHeader bool `json:"noGRPCHeader"` NoGRPCHeader bool `json:"noGRPCHeader"`
NoSSEHeader bool `json:"noSSEHeader"` NoSSEHeader bool `json:"noSSEHeader"`
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"` ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"` ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"` ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"` ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"`
Xmux XmuxConfig `json:"xmux"` Xmux XmuxConfig `json:"xmux"`
DownloadSettings *StreamConfig `json:"downloadSettings"` DownloadSettings *StreamConfig `json:"downloadSettings"`
Extra json.RawMessage `json:"extra"` Extra json.RawMessage `json:"extra"`
@@ -316,9 +317,9 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
switch c.UplinkDataPlacement { switch c.UplinkDataPlacement {
case "": case "":
c.UplinkDataPlacement = "body" c.UplinkDataPlacement = splithttp.PlacementAuto
case "body": case splithttp.PlacementAuto, splithttp.PlacementBody:
case "cookie", "header": case splithttp.PlacementCookie, splithttp.PlacementHeader:
if c.Mode != "packet-up" { if c.Mode != "packet-up" {
return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode") return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode")
} }
@@ -347,9 +348,6 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
case "": case "":
c.SeqPlacement = "path" c.SeqPlacement = "path"
case "path", "cookie", "header", "query": case "path", "cookie", "header", "query":
if c.SessionPlacement == "path" {
return nil, errors.New("SeqPlacement must be path when SessionPlacement is path")
}
default: default:
return nil, errors.New("unsupported seq placement: " + c.SeqPlacement) return nil, errors.New("unsupported seq placement: " + c.SeqPlacement)
} }
@@ -372,24 +370,17 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
} }
} }
if c.UplinkDataPlacement != "body" && c.UplinkDataKey == "" { if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" {
switch c.UplinkDataPlacement { switch c.UplinkDataPlacement {
case "cookie": case splithttp.PlacementCookie:
c.UplinkDataKey = "x_data" c.UplinkDataKey = "x_data"
case "header": case splithttp.PlacementAuto, splithttp.PlacementHeader:
c.UplinkDataKey = "X-Data" c.UplinkDataKey = "X-Data"
} }
} }
if c.UplinkChunkSize == 0 { if c.ServerMaxHeaderBytes < 0 {
switch c.UplinkDataPlacement { return nil, errors.New("invalid negative value of maxHeaderBytes")
case "cookie":
c.UplinkChunkSize = 3 * 1024 // 3KB
case "header":
c.UplinkChunkSize = 4 * 1024 // 4KB
}
} else if c.UplinkChunkSize < 64 {
c.UplinkChunkSize = 64
} }
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 { if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
@@ -422,13 +413,14 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
SeqKey: c.SeqKey, SeqKey: c.SeqKey,
UplinkDataPlacement: c.UplinkDataPlacement, UplinkDataPlacement: c.UplinkDataPlacement,
UplinkDataKey: c.UplinkDataKey, UplinkDataKey: c.UplinkDataKey,
UplinkChunkSize: c.UplinkChunkSize, UplinkChunkSize: newRangeConfig(c.UplinkChunkSize),
NoGRPCHeader: c.NoGRPCHeader, NoGRPCHeader: c.NoGRPCHeader,
NoSSEHeader: c.NoSSEHeader, NoSSEHeader: c.NoSSEHeader,
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes), ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs), ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
ScMaxBufferedPosts: c.ScMaxBufferedPosts, ScMaxBufferedPosts: c.ScMaxBufferedPosts,
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs), ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
ServerMaxHeaderBytes: c.ServerMaxHeaderBytes,
Xmux: &splithttp.XmuxConfig{ Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections), MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
+23 -6
View File
@@ -22,6 +22,7 @@ type task struct {
Method string `json:"method"` Method string `json:"method"`
URL string `json:"url"` URL string `json:"url"`
Extra any `json:"extra,omitempty"` Extra any `json:"extra,omitempty"`
StreamResponse bool `json:"streamResponse"`
} }
var conns chan *websocket.Conn var conns chan *websocket.Conn
@@ -52,6 +53,7 @@ func init() {
} }
} }
} else { } else {
w.Header().Set("Access-Control-Allow-Origin", "*");
w.Write(webpage) w.Write(webpage)
} }
})) }))
@@ -70,6 +72,7 @@ func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
task := task{ task := task{
Method: "WS", Method: "WS",
URL: uri, URL: uri,
StreamResponse: true,
} }
if ed != nil { if ed != nil {
@@ -84,9 +87,10 @@ func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
type httpExtra struct { type httpExtra struct {
Referrer string `json:"referrer,omitempty"` Referrer string `json:"referrer,omitempty"`
Headers map[string]string `json:"headers,omitempty"` Headers map[string]string `json:"headers,omitempty"`
Cookies map[string]string `json:"cookies,omitempty"`
} }
func httpExtraFromHeaders(headers http.Header) *httpExtra { func httpExtraFromHeadersAndCookies(headers http.Header, cookies []*http.Cookie) *httpExtra {
if len(headers) == 0 { if len(headers) == 0 {
return nil return nil
} }
@@ -104,24 +108,37 @@ func httpExtraFromHeaders(headers http.Header) *httpExtra {
} }
} }
if len(cookies) > 0 {
extra.Cookies = make(map[string]string)
for _, cookie := range cookies {
extra.Cookies[cookie.Name] = cookie.Value
}
}
return &extra return &extra
} }
func DialGet(uri string, headers http.Header) (*websocket.Conn, error) { func DialGet(uri string, headers http.Header, cookies []*http.Cookie) (*websocket.Conn, error) {
task := task{ task := task{
Method: "GET", Method: "GET",
URL: uri, URL: uri,
Extra: httpExtraFromHeaders(headers), Extra: httpExtraFromHeadersAndCookies(headers, cookies),
StreamResponse: true,
} }
return dialTask(task) return dialTask(task)
} }
func DialPost(uri string, headers http.Header, payload []byte) error { func DialPacket(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error {
return dialWithBody(method, uri, headers, cookies, payload)
}
func dialWithBody(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error {
task := task{ task := task{
Method: "POST", Method: method,
URL: uri, URL: uri,
Extra: httpExtraFromHeaders(headers), Extra: httpExtraFromHeadersAndCookies(headers, cookies),
StreamResponse: false,
} }
conn, err := dialTask(task) conn, err := dialTask(task)
+139 -105
View File
@@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<title>Browser Dialer</title> <title>Browser Dialer</title>
<link rel="icon" href="data:">
</head> </head>
<body> <body>
<script> <script>
@@ -29,9 +30,37 @@
requestInit.headers = extra.headers; requestInit.headers = extra.headers;
} }
if (extra.cookies) {
requestInit.credentials = 'include';
}
return requestInit; return requestInit;
} }
function setCookiesFromTask(task) {
if (!task.extra.cookies) {
return;
}
const url = new URL(task.url);
for (const [name, value] of Object.entries(task.extra.cookies)) {
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + '; path=' + url.pathname;
}
}
function clearCookiesFromTask(task) {
if (!task.extra.cookies) {
return;
}
const url = new URL(task.url);
for (const [name, value] of Object.entries(task.extra.cookies)) {
document.cookie = encodeURIComponent(name) + '=; path=' + url.pathname + '; Max-Age=0';
}
}
let check = function () { let check = function () {
if (clientIdleCount > 0) { if (clientIdleCount > 0) {
return; return;
@@ -48,116 +77,121 @@
ws.onmessage = function (event) { ws.onmessage = function (event) {
clientIdleCount -= 1; clientIdleCount -= 1;
let task = JSON.parse(event.data); let task = JSON.parse(event.data);
switch (task.method) { if (task.method == "WS") {
case "WS": { upstreamWsCount += 1;
upstreamWsCount += 1; console.log("Dial WS", task.url, task.extra.protocol);
console.log("Dial WS", task.url, task.extra.protocol); const wss = new WebSocket(task.url, task.extra.protocol);
const wss = new WebSocket(task.url, task.extra.protocol); wss.binaryType = "arraybuffer";
wss.binaryType = "arraybuffer"; let opened = false;
let opened = false; ws.onmessage = function (event) {
ws.onmessage = function (event) { wss.send(event.data)
wss.send(event.data) };
}; wss.onopen = function (event) {
wss.onopen = function (event) { opened = true;
opened = true; ws.send("ok")
ws.send("ok") };
}; wss.onmessage = function (event) {
wss.onmessage = function (event) { ws.send(event.data)
ws.send(event.data) };
}; wss.onclose = function (event) {
wss.onclose = function (event) { upstreamWsCount -= 1;
upstreamWsCount -= 1; console.log("Dial WS DONE, remaining: ", upstreamWsCount);
console.log("Dial WS DONE, remaining: ", upstreamWsCount); ws.close()
ws.close() };
}; wss.onerror = function (event) {
wss.onerror = function (event) { !opened && ws.send("fail")
!opened && ws.send("fail") wss.close()
wss.close() };
}; ws.onclose = function (event) {
ws.onclose = function (event) { wss.close()
wss.close() };
}; }
break; else if (task.method == "GET" && task.streamResponse) {
} (async () => {
case "GET": {
(async () => {
const requestInit = prepareRequestInit(task.extra);
console.log("Dial GET", task.url);
ws.send("ok");
const controller = new AbortController();
/*
Aborting a streaming response in JavaScript
requires two levers to be pulled:
First, the streaming read itself has to be cancelled using
reader.cancel(), only then controller.abort() will actually work.
If controller.abort() alone is called while a
reader.read() is ongoing, it will block until the server closes the
response, the page is refreshed or the network connection is lost.
*/
let reader = null;
ws.onclose = (event) => {
try {
reader && reader.cancel();
} catch(e) {}
try {
controller.abort();
} catch(e) {}
};
try {
upstreamGetCount += 1;
requestInit.signal = controller.signal;
const response = await fetch(task.url, requestInit);
const body = await response.body;
reader = body.getReader();
while (true) {
const { done, value } = await reader.read();
if (value) ws.send(value); // don't send back "undefined" string when received nothing
if (done) break;
}
} finally {
upstreamGetCount -= 1;
console.log("Dial GET DONE, remaining: ", upstreamGetCount);
ws.close();
}
})();
break;
}
case "POST": {
upstreamPostCount += 1;
const requestInit = prepareRequestInit(task.extra); const requestInit = prepareRequestInit(task.extra);
requestInit.method = "POST";
console.log("Dial POST", task.url); console.log("Dial GET", task.url);
ws.send("ok"); ws.send("ok");
ws.onmessage = async (event) => { const controller = new AbortController();
/*
Aborting a streaming response in JavaScript
requires two levers to be pulled:
First, the streaming read itself has to be cancelled using
reader.cancel(), only then controller.abort() will actually work.
If controller.abort() alone is called while a
reader.read() is ongoing, it will block until the server closes the
response, the page is refreshed or the network connection is lost.
*/
let reader = null;
ws.onclose = (event) => {
try { try {
requestInit.body = event.data; reader && reader.cancel();
const response = await fetch(task.url, requestInit); } catch(e) {}
if (response.ok) {
ws.send("ok"); try {
} else { controller.abort();
console.error("bad status code"); } catch(e) {}
ws.send("fail");
}
} finally {
upstreamPostCount -= 1;
console.log("Dial POST DONE, remaining: ", upstreamPostCount);
ws.close();
}
}; };
break;
} try {
upstreamGetCount += 1;
requestInit.signal = controller.signal;
setCookiesFromTask(task);
const response = await fetch(task.url, requestInit);
clearCookiesFromTask(task);
const body = await response.body;
reader = body.getReader();
while (true) {
const { done, value } = await reader.read();
if (value) ws.send(value); // don't send back "undefined" string when received nothing
if (done) break;
}
} finally {
upstreamGetCount -= 1;
console.log("Dial GET DONE, remaining: ", upstreamGetCount);
ws.close();
}
})();
}
else if (!task.streamResponse) {
upstreamPostCount += 1;
const requestInit = prepareRequestInit(task.extra);
requestInit.method = task.method;
console.log("Dial", task.method, task.url);
ws.send("ok");
ws.onmessage = async (event) => {
try {
if (event.data.byteLength > 0) {
requestInit.body = event.data;
}
setCookiesFromTask(task);
const response = await fetch(task.url, requestInit);
clearCookiesFromTask(task);
if (response.ok) {
ws.send("ok");
} else {
console.error("bad status code");
ws.send("fail");
}
} finally {
upstreamPostCount -= 1;
console.log("Dial", task.method, "packet DONE, remaining: ", upstreamPostCount);
ws.close();
}
};
}
else {
console.error(`Incorrect task method=${task.method} streamResponse=${task.streamResponse}.`);
ws.close();
} }
check(); check();
+21 -44
View File
@@ -3,6 +3,7 @@ package splithttp
import ( import (
"context" "context"
"io" "io"
"net/http"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
@@ -19,35 +20,19 @@ func (c *BrowserDialerClient) IsClosed() bool {
panic("not implemented yet") 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 { if body != nil {
return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet") return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet")
} }
header := c.transportConfig.GetRequestHeader() request, err := http.NewRequest("GET", url, nil)
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand()) if err != nil {
config := XPaddingConfig{Length: length} return nil, nil, nil, 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,
}
} }
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{} dummyAddr := &net.IPAddr{}
if err != nil { if err != nil {
return nil, dummyAddr, dummyAddr, err 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 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 { func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
bytes, err := io.ReadAll(body) method := c.transportConfig.GetNormalizedUplinkHTTPMethod()
request, err := http.NewRequest(method, url, body)
if err != nil { if err != nil {
return err return err
} }
header := c.transportConfig.GetRequestHeader() request.ContentLength = contentLength
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand()) err = c.transportConfig.FillPacketRequest(request, sessionId, seqStr)
config := XPaddingConfig{Length: length} if err != nil {
return err
}
if c.transportConfig.XPaddingObfsMode { var bytes []byte
config.Placement = XPaddingPlacement{ if (request.Body != nil) {
Placement: c.transportConfig.XPaddingPlacement, bytes, err = io.ReadAll(request.Body)
Key: c.transportConfig.XPaddingKey, if err != nil {
Header: c.transportConfig.XPaddingHeader, return err
RawURL: url,
}
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
} }
} }
c.transportConfig.ApplyXPaddingToHeader(header, config) err = browser_dialer.DialPacket(method, request.URL.String(), request.Header, request.Cookies(), bytes)
err = browser_dialer.DialPost(url, header, bytes)
if err != nil { if err != nil {
return err return err
} }
+2 -98
View File
@@ -3,7 +3,6 @@ package splithttp
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -60,33 +59,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
method = c.transportConfig.GetNormalizedUplinkHTTPMethod() // stream-up/one method = c.transportConfig.GetNormalizedUplinkHTTPMethod() // stream-up/one
} }
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
req.Header = c.transportConfig.GetRequestHeader() c.transportConfig.FillStreamRequest(req, sessionId, "")
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")
}
wrc = &WaitReadCloser{Wait: make(chan struct{})} wrc = &WaitReadCloser{Wait: make(chan struct{})}
go func() { 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 { 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() method := c.transportConfig.GetNormalizedUplinkHTTPMethod()
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
if err != nil { if err != nil {
return err return err
} }
req.ContentLength = contentLength req.ContentLength = contentLength
req.Header = c.transportConfig.GetRequestHeader() c.transportConfig.FillPacketRequest(req, sessionId, seqStr)
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)
if c.httpVersion != "1.1" { if c.httpVersion != "1.1" {
resp, err := c.client.Do(req) resp, err := c.client.Do(req)
+1
View File
@@ -7,4 +7,5 @@ const (
PlacementQuery = "query" PlacementQuery = "query"
PlacementPath = "path" PlacementPath = "path"
PlacementBody = "body" PlacementBody = "body"
PlacementAuto = "auto"
) )
+199 -13
View File
@@ -1,6 +1,9 @@
package splithttp package splithttp
import ( import (
"encoding/base64"
"fmt"
"io"
"net/http" "net/http"
"strings" "strings"
@@ -54,11 +57,72 @@ func (c *Config) GetRequestHeader() http.Header {
return 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 // CORS headers for the browser dialer
writer.Header().Set("Access-Control-Allow-Origin", "*") if origin := requestHeader.Get("Origin"); origin == "" {
writer.Header().Set("Access-Control-Allow-Methods", "*") writer.Header().Set("Access-Control-Allow-Origin", "*")
// writer.Header().Set("X-Version", core.Version()) } 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 { func (c *Config) GetNormalizedUplinkHTTPMethod() string {
@@ -110,6 +174,40 @@ func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig {
return *c.ScStreamUpServerSecs 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 { func (c *Config) GetNormalizedSessionPlacement() string {
if c.SessionPlacement == "" { if c.SessionPlacement == "" {
return PlacementPath 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) { func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) {
sessionPlacement := c.GetNormalizedSessionPlacement() sessionPlacement := c.GetNormalizedSessionPlacement()
seqPlacement := c.GetNormalizedSeqPlacement() seqPlacement := c.GetNormalizedSeqPlacement()
sessionKey := c.GetNormalizedSessionKey() sessionKey := c.GetNormalizedSessionKey()
seqKey := c.GetNormalizedSeqKey() seqKey := c.GetNormalizedSeqKey()
if sessionPlacement == PlacementPath && seqPlacement == PlacementPath { var subpath []string
subpath := strings.Split(req.URL.Path[len(path):], "/") pathPart := 0
if len(subpath) > 0 { if sessionPlacement == PlacementPath || seqPlacement == PlacementPath {
sessionId = subpath[0] subpath = strings.Split(req.URL.Path[len(path):], "/")
}
if len(subpath) > 1 {
seqStr = subpath[1]
}
return sessionId, seqStr
} }
switch sessionPlacement { switch sessionPlacement {
case PlacementPath:
if len(subpath) > pathPart {
sessionId = subpath[pathPart]
pathPart += 1
}
case PlacementQuery: case PlacementQuery:
sessionId = req.URL.Query().Get(sessionKey) sessionId = req.URL.Query().Get(sessionKey)
case PlacementHeader: case PlacementHeader:
@@ -225,6 +406,11 @@ func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (session
} }
switch seqPlacement { switch seqPlacement {
case PlacementPath:
if len(subpath) > pathPart {
seqStr = subpath[pathPart]
pathPart += 1
}
case PlacementQuery: case PlacementQuery:
seqStr = req.URL.Query().Get(seqKey) seqStr = req.URL.Query().Get(seqKey)
case PlacementHeader: case PlacementHeader:
+20 -11
View File
@@ -185,7 +185,8 @@ type Config struct {
SeqKey string `protobuf:"bytes,23,opt,name=seqKey,proto3" json:"seqKey,omitempty"` SeqKey string `protobuf:"bytes,23,opt,name=seqKey,proto3" json:"seqKey,omitempty"`
UplinkDataPlacement string `protobuf:"bytes,24,opt,name=uplinkDataPlacement,proto3" json:"uplinkDataPlacement,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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -395,10 +396,17 @@ func (x *Config) GetUplinkDataKey() string {
return "" return ""
} }
func (x *Config) GetUplinkChunkSize() uint32 { func (x *Config) GetUplinkChunkSize() *RangeConfig {
if x != nil { if x != nil {
return x.UplinkChunkSize return x.UplinkChunkSize
} }
return nil
}
func (x *Config) GetServerMaxHeaderBytes() int32 {
if x != nil {
return x.ServerMaxHeaderBytes
}
return 0 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" + "\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" + "\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" + "\x10hMaxReusableSecs\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxReusableSecs\x12*\n" +
"\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xde\n" + "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xc2\v\n" +
"\n" +
"\x06Config\x12\x12\n" + "\x06Config\x12\x12\n" +
"\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\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" + "\fseqPlacement\x18\x16 \x01(\tR\fseqPlacement\x12\x16\n" +
"\x06seqKey\x18\x17 \x01(\tR\x06seqKey\x120\n" + "\x06seqKey\x18\x17 \x01(\tR\x06seqKey\x120\n" +
"\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" + "\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" +
"\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12(\n" + "\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12X\n" +
"\x0fuplinkChunkSize\x18\x1a \x01(\rR\x0fuplinkChunkSize\x1a:\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" + "\fHeadersEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\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 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 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 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 0, // 12: xray.transport.internet.splithttp.Config.uplinkChunkSize:type_name -> xray.transport.internet.splithttp.RangeConfig
12, // [12:12] is the sub-list for method input_type 13, // [13:13] is the sub-list for method output_type
12, // [12:12] is the sub-list for extension type_name 13, // [13:13] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension extendee 13, // [13:13] is the sub-list for extension type_name
0, // [0:12] is the sub-list for field 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() } func init() { file_transport_internet_splithttp_config_proto_init() }
+2 -1
View File
@@ -48,5 +48,6 @@ message Config {
string seqKey = 23; string seqKey = 23;
string uplinkDataPlacement = 24; string uplinkDataPlacement = 24;
string uplinkDataKey = 25; string uplinkDataKey = 25;
uint32 uplinkChunkSize = 26; RangeConfig uplinkChunkSize = 26;
int32 serverMaxHeaderBytes = 27;
} }
+52 -45
View File
@@ -396,8 +396,8 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
scMaxEachPostBytes := transportConfiguration.GetNormalizedScMaxEachPostBytes() scMaxEachPostBytes := transportConfiguration.GetNormalizedScMaxEachPostBytes()
scMinPostsIntervalMs := transportConfiguration.GetNormalizedScMinPostsIntervalMs() scMinPostsIntervalMs := transportConfiguration.GetNormalizedScMinPostsIntervalMs()
if scMaxEachPostBytes.From <= buf.Size { if scMaxEachPostBytes.From <= 0 {
panic("`scMaxEachPostBytes` should be bigger than " + strconv.Itoa(buf.Size)) panic("`scMaxEachPostBytes` should be bigger than 0")
} }
maxUploadSize := scMaxEachPostBytes.rand() 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 // code relies on this behavior. Subtract 1 so that together with
// uploadWriter wrapper, exact size limits can be enforced // uploadWriter wrapper, exact size limits can be enforced
// uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize - 1)) // 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{ conn.writer = uploadWriter{
uploadPipeWriter, uploadPipeWriter,
@@ -417,57 +417,64 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
var lastWrite time.Time var lastWrite time.Time
for { 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 // by offloading the uploads into a buffered pipe, multiple conn.Write
// calls get automatically batched together into larger POST requests. // calls get automatically batched together into larger POST requests.
// without batching, bandwidth is extremely limited. // without batching, bandwidth is extremely limited.
chunk, err := uploadPipeReader.ReadMultiBuffer() remainder, err := uploadPipeReader.ReadMultiBuffer()
if err != nil { if err != nil {
break break
} }
lastWrite = time.Now() doSplit := atomic.Bool{}
for doSplit.Store(true); doSplit.Load(); {
if xmuxClient != nil && (xmuxClient.LeftRequests.Add(-1) <= 0 || var chunk buf.MultiBuffer
(xmuxClient.UnreusableAt != time.Time{} && lastWrite.After(xmuxClient.UnreusableAt))) { remainder, chunk = buf.SplitSize(remainder, maxUploadSize)
httpClient, xmuxClient = getHTTPClient(ctx, dest, streamSettings) if chunk.IsEmpty() {
} break
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()
} }
}()
if _, ok := httpClient.(*DefaultDialerClient); ok { wroteRequest := done.New()
<-wroteRequest.Wait()
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()
}
} }
} }
}() }()
+60 -70
View File
@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -100,7 +101,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return return
} }
h.config.WriteResponseHeader(writer) h.config.WriteResponseHeader(writer, request.Method, request.Header)
length := int(h.config.GetNormalizedXPaddingBytes().rand()) length := int(h.config.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length} 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} clientVer := []int{0, 0, 0}
@@ -183,27 +189,17 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
currentSession = h.upsertSession(sessionId) currentSession = h.upsertSession(sessionId)
} }
scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To) scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To)
uplinkHTTPMethod := h.config.GetNormalizedUplinkHTTPMethod()
isUplinkRequest := false isUplinkRequest := false
if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod { switch request.Method {
case "GET":
isUplinkRequest = seqStr != ""
default:
isUplinkRequest = true isUplinkRequest = true
} }
uplinkDataPlacement := h.config.GetNormalizedUplinkDataPlacement()
uplinkDataKey := h.config.UplinkDataKey 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 isUplinkRequest && sessionId != "" { // stream-up, packet-up
if seqStr == "" { if seqStr == "" {
if h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-up" { 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 return
} }
var payload []byte dataPlacement := h.config.GetNormalizedUplinkDataPlacement()
var headerPayload []byte
if uplinkDataPlacement != PlacementBody { if dataPlacement == PlacementAuto || dataPlacement == PlacementHeader {
var encodedStr string var headerPayloadChunks [] string
switch uplinkDataPlacement { for i := 0; true; i++ {
case PlacementHeader: chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
dataLenStr := request.Header.Get(uplinkDataKey + "-Length") if chunk == "" {
break
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 = ""
}
} }
case PlacementCookie: headerPayloadChunks = append(headerPayloadChunks, chunk)
var chunks []string }
i := 0 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 { var cookiePayload []byte
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i) if dataPlacement == PlacementAuto || dataPlacement == PlacementCookie {
if c, _ := request.Cookie(cookieName); c != nil { var cookiePayloadChunks []string
chunks = append(chunks, c.Value) for i := 0; true; i++ {
i++ cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
} else { if c, _ := request.Cookie(cookieName); c != nil {
break cookiePayloadChunks = append(cookiePayloadChunks, c.Value)
} } else {
} break
if len(chunks) > 0 {
encodedStr = strings.Join(chunks, "")
} }
} }
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 != "" { var bodyPayload []byte
payload, err = base64.RawURLEncoding.DecodeString(encodedStr) if dataPlacement == PlacementAuto || dataPlacement == PlacementBody {
} else { bodyPayload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
errors.LogInfoInner(context.Background(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement) if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to upload (ReadAll)")
writer.WriteHeader(http.StatusInternalServerError) writer.WriteHeader(http.StatusInternalServerError)
return return
} }
} else {
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
} }
payload := slices.Concat(headerPayload, cookiePayload, bodyPayload)
if len(payload) > scMaxEachPostBytes { 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.") 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) writer.WriteHeader(http.StatusRequestEntityTooLarge)
return 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) seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil { if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)") errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)")
@@ -341,6 +326,11 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return 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) writer.WriteHeader(http.StatusOK)
} else if request.Method == "GET" || sessionId == "" { // stream-down, stream-one } else if request.Method == "GET" || sessionId == "" { // stream-down, stream-one
if sessionId != "" { if sessionId != "" {
@@ -519,7 +509,7 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet
l.server = http.Server{ l.server = http.Server{
Handler: handler, Handler: handler,
ReadHeaderTimeout: time.Second * 4, ReadHeaderTimeout: time.Second * 4,
MaxHeaderBytes: 8192, MaxHeaderBytes: l.config.GetNormalizedServerMaxHeaderBytes(),
Protocols: protocols, Protocols: protocols,
} }
go func() { go func() {
+27
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) { func ApplyPaddingToQuery(u *url.URL, key, value string) {
if u == nil || key == "" || value == "" { if u == nil || key == "" || value == "" {
return 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) { func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) {
if req == nil { if req == nil {
return "", "" return "", ""