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:
@@ -230,13 +230,14 @@ type SplitHTTPConfig struct {
|
||||
SeqKey string `json:"seqKey"`
|
||||
UplinkDataPlacement string `json:"uplinkDataPlacement"`
|
||||
UplinkDataKey string `json:"uplinkDataKey"`
|
||||
UplinkChunkSize uint32 `json:"uplinkChunkSize"`
|
||||
UplinkChunkSize Int32Range `json:"uplinkChunkSize"`
|
||||
NoGRPCHeader bool `json:"noGRPCHeader"`
|
||||
NoSSEHeader bool `json:"noSSEHeader"`
|
||||
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
|
||||
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
|
||||
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
|
||||
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
|
||||
ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"`
|
||||
Xmux XmuxConfig `json:"xmux"`
|
||||
DownloadSettings *StreamConfig `json:"downloadSettings"`
|
||||
Extra json.RawMessage `json:"extra"`
|
||||
@@ -316,9 +317,9 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
|
||||
|
||||
switch c.UplinkDataPlacement {
|
||||
case "":
|
||||
c.UplinkDataPlacement = "body"
|
||||
case "body":
|
||||
case "cookie", "header":
|
||||
c.UplinkDataPlacement = splithttp.PlacementAuto
|
||||
case splithttp.PlacementAuto, splithttp.PlacementBody:
|
||||
case splithttp.PlacementCookie, splithttp.PlacementHeader:
|
||||
if c.Mode != "packet-up" {
|
||||
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 "":
|
||||
c.SeqPlacement = "path"
|
||||
case "path", "cookie", "header", "query":
|
||||
if c.SessionPlacement == "path" {
|
||||
return nil, errors.New("SeqPlacement must be path when SessionPlacement is path")
|
||||
}
|
||||
default:
|
||||
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 {
|
||||
case "cookie":
|
||||
case splithttp.PlacementCookie:
|
||||
c.UplinkDataKey = "x_data"
|
||||
case "header":
|
||||
case splithttp.PlacementAuto, splithttp.PlacementHeader:
|
||||
c.UplinkDataKey = "X-Data"
|
||||
}
|
||||
}
|
||||
|
||||
if c.UplinkChunkSize == 0 {
|
||||
switch c.UplinkDataPlacement {
|
||||
case "cookie":
|
||||
c.UplinkChunkSize = 3 * 1024 // 3KB
|
||||
case "header":
|
||||
c.UplinkChunkSize = 4 * 1024 // 4KB
|
||||
}
|
||||
} else if c.UplinkChunkSize < 64 {
|
||||
c.UplinkChunkSize = 64
|
||||
if c.ServerMaxHeaderBytes < 0 {
|
||||
return nil, errors.New("invalid negative value of maxHeaderBytes")
|
||||
}
|
||||
|
||||
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,
|
||||
UplinkDataPlacement: c.UplinkDataPlacement,
|
||||
UplinkDataKey: c.UplinkDataKey,
|
||||
UplinkChunkSize: c.UplinkChunkSize,
|
||||
UplinkChunkSize: newRangeConfig(c.UplinkChunkSize),
|
||||
NoGRPCHeader: c.NoGRPCHeader,
|
||||
NoSSEHeader: c.NoSSEHeader,
|
||||
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
|
||||
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
|
||||
ScMaxBufferedPosts: c.ScMaxBufferedPosts,
|
||||
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
|
||||
ServerMaxHeaderBytes: c.ServerMaxHeaderBytes,
|
||||
Xmux: &splithttp.XmuxConfig{
|
||||
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
|
||||
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
|
||||
|
||||
@@ -22,6 +22,7 @@ type task struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Extra any `json:"extra,omitempty"`
|
||||
StreamResponse bool `json:"streamResponse"`
|
||||
}
|
||||
|
||||
var conns chan *websocket.Conn
|
||||
@@ -52,6 +53,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*");
|
||||
w.Write(webpage)
|
||||
}
|
||||
}))
|
||||
@@ -70,6 +72,7 @@ func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
|
||||
task := task{
|
||||
Method: "WS",
|
||||
URL: uri,
|
||||
StreamResponse: true,
|
||||
}
|
||||
|
||||
if ed != nil {
|
||||
@@ -84,9 +87,10 @@ func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
|
||||
type httpExtra struct {
|
||||
Referrer string `json:"referrer,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 {
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
Method: "GET",
|
||||
URL: uri,
|
||||
Extra: httpExtraFromHeaders(headers),
|
||||
Extra: httpExtraFromHeadersAndCookies(headers, cookies),
|
||||
StreamResponse: true,
|
||||
}
|
||||
|
||||
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{
|
||||
Method: "POST",
|
||||
Method: method,
|
||||
URL: uri,
|
||||
Extra: httpExtraFromHeaders(headers),
|
||||
Extra: httpExtraFromHeadersAndCookies(headers, cookies),
|
||||
StreamResponse: false,
|
||||
}
|
||||
|
||||
conn, err := dialTask(task)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Browser Dialer</title>
|
||||
<link rel="icon" href="data:">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
@@ -29,9 +30,37 @@
|
||||
requestInit.headers = extra.headers;
|
||||
}
|
||||
|
||||
if (extra.cookies) {
|
||||
requestInit.credentials = 'include';
|
||||
}
|
||||
|
||||
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 () {
|
||||
if (clientIdleCount > 0) {
|
||||
return;
|
||||
@@ -48,116 +77,121 @@
|
||||
ws.onmessage = function (event) {
|
||||
clientIdleCount -= 1;
|
||||
let task = JSON.parse(event.data);
|
||||
switch (task.method) {
|
||||
case "WS": {
|
||||
upstreamWsCount += 1;
|
||||
console.log("Dial WS", task.url, task.extra.protocol);
|
||||
const wss = new WebSocket(task.url, task.extra.protocol);
|
||||
wss.binaryType = "arraybuffer";
|
||||
let opened = false;
|
||||
ws.onmessage = function (event) {
|
||||
wss.send(event.data)
|
||||
};
|
||||
wss.onopen = function (event) {
|
||||
opened = true;
|
||||
ws.send("ok")
|
||||
};
|
||||
wss.onmessage = function (event) {
|
||||
ws.send(event.data)
|
||||
};
|
||||
wss.onclose = function (event) {
|
||||
upstreamWsCount -= 1;
|
||||
console.log("Dial WS DONE, remaining: ", upstreamWsCount);
|
||||
ws.close()
|
||||
};
|
||||
wss.onerror = function (event) {
|
||||
!opened && ws.send("fail")
|
||||
wss.close()
|
||||
};
|
||||
ws.onclose = function (event) {
|
||||
wss.close()
|
||||
};
|
||||
break;
|
||||
}
|
||||
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;
|
||||
|
||||
if (task.method == "WS") {
|
||||
upstreamWsCount += 1;
|
||||
console.log("Dial WS", task.url, task.extra.protocol);
|
||||
const wss = new WebSocket(task.url, task.extra.protocol);
|
||||
wss.binaryType = "arraybuffer";
|
||||
let opened = false;
|
||||
ws.onmessage = function (event) {
|
||||
wss.send(event.data)
|
||||
};
|
||||
wss.onopen = function (event) {
|
||||
opened = true;
|
||||
ws.send("ok")
|
||||
};
|
||||
wss.onmessage = function (event) {
|
||||
ws.send(event.data)
|
||||
};
|
||||
wss.onclose = function (event) {
|
||||
upstreamWsCount -= 1;
|
||||
console.log("Dial WS DONE, remaining: ", upstreamWsCount);
|
||||
ws.close()
|
||||
};
|
||||
wss.onerror = function (event) {
|
||||
!opened && ws.send("fail")
|
||||
wss.close()
|
||||
};
|
||||
ws.onclose = function (event) {
|
||||
wss.close()
|
||||
};
|
||||
}
|
||||
else if (task.method == "GET" && task.streamResponse) {
|
||||
(async () => {
|
||||
const requestInit = prepareRequestInit(task.extra);
|
||||
requestInit.method = "POST";
|
||||
|
||||
console.log("Dial POST", task.url);
|
||||
console.log("Dial GET", task.url);
|
||||
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 {
|
||||
requestInit.body = event.data;
|
||||
const response = await fetch(task.url, requestInit);
|
||||
if (response.ok) {
|
||||
ws.send("ok");
|
||||
} else {
|
||||
console.error("bad status code");
|
||||
ws.send("fail");
|
||||
}
|
||||
} finally {
|
||||
upstreamPostCount -= 1;
|
||||
console.log("Dial POST DONE, remaining: ", upstreamPostCount);
|
||||
ws.close();
|
||||
}
|
||||
reader && reader.cancel();
|
||||
} catch(e) {}
|
||||
|
||||
try {
|
||||
controller.abort();
|
||||
} catch(e) {}
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -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