mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-07-06 03:38:47 +00:00
XHTTP transport: Bugfixes for obfuscations (#5720)
https://github.com/XTLS/Xray-core/pull/5720#issuecomment-4016290343
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user