-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Vahid Farid
committed
May 31, 2024
1 parent
af18572
commit 48a4f7b
Showing
1 changed file
with
321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
import { connect } from 'cloudflare:sockets' | ||
import { GetTrojanConfig, MuddleDomain, getSHA224Password } from "./helpers" | ||
import { cfPorts } from "./variables" | ||
import { RemoteSocketWrapper, CustomArrayBuffer, VlessHeader, UDPOutbound, Config, Env } from "./interfaces" | ||
|
||
const WS_READY_STATE_OPEN: number = 1 | ||
const WS_READY_STATE_CLOSING: number = 2 | ||
let proxyIP: string = "" | ||
let proxyList: Array<string> = [] | ||
let filterCountries: string = "" | ||
let countries: Array<string> = [] | ||
|
||
export async function GetTrojanConfigList(sni: string, addressList: Array<string>, start: number, max: number, env: Env) { | ||
filterCountries = "" | ||
proxyList = [] | ||
const sha224Password: string = getSHA224Password(sni) | ||
let configList: Array<Config> = [] | ||
for (let i = 0; i < max; i++) { | ||
configList.push(GetTrojanConfig( | ||
i + start, | ||
sha224Password, | ||
MuddleDomain(sni), | ||
addressList[Math.floor(Math.random() * addressList.length)], | ||
cfPorts[Math.floor(Math.random() * cfPorts.length)] | ||
)) | ||
} | ||
|
||
return configList | ||
} | ||
|
||
export async function TrojanOverWSHandler(request: Request, sni: string, env: Env) { | ||
const sha224Password = getSHA224Password(sni) | ||
const [client, webSocket]: Array<WebSocket> = Object.values(new WebSocketPair) | ||
webSocket.accept() | ||
|
||
let address: string = "" | ||
const earlyDataHeader: string = request.headers.get("sec-websocket-protocol") || "" | ||
const readableWebSocketStream = MakeReadableWebSocketStream(webSocket, earlyDataHeader) | ||
|
||
let remoteSocketWapper: RemoteSocketWrapper = { | ||
value: null, | ||
} | ||
|
||
let portWithRandomLog = ""; | ||
|
||
readableWebSocketStream.pipeTo(new WritableStream({ | ||
async write(chunk, controller) { | ||
if (remoteSocketWapper.value) { | ||
const writer = remoteSocketWapper.value.writable.getWriter(); | ||
await writer.write(chunk); | ||
writer.releaseLock(); | ||
return; | ||
} | ||
const { | ||
hasError, | ||
message, | ||
portRemote = 443, | ||
addressRemote = "", | ||
rawDataIndex | ||
} = await ParseTrojanHeader(chunk, sha224Password); | ||
address = addressRemote; | ||
portWithRandomLog = `${portRemote}--${Math.random()} tcp`; | ||
if (hasError) { | ||
throw new Error(message); | ||
} | ||
const rawClientData: Uint8Array = chunk.slice(rawDataIndex) | ||
HandleTCPOutbound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, env); | ||
}, | ||
})).catch((err) => { }); | ||
return new Response(null, { | ||
status: 101, | ||
// @ts-ignore | ||
webSocket: client | ||
}); | ||
} | ||
|
||
async function ParseTrojanHeader(buffer: ArrayBuffer, sha224Password: string) { | ||
if (buffer.byteLength < 56) { | ||
return { | ||
hasError: true, | ||
message: "invalid data" | ||
}; | ||
} | ||
let crLfIndex: number = 56; | ||
if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) { | ||
return { | ||
hasError: true, | ||
message: "invalid header format (missing CR LF)" | ||
}; | ||
} | ||
const password: string = new TextDecoder().decode(buffer.slice(0, crLfIndex)); | ||
if (password !== sha224Password) { | ||
return { | ||
hasError: true, | ||
message: "invalid password" | ||
}; | ||
} | ||
|
||
const socks5DataBuffer: ArrayBuffer = buffer.slice(crLfIndex + 2); | ||
if (socks5DataBuffer.byteLength < 6) { | ||
return { | ||
hasError: true, | ||
message: "invalid SOCKS5 request data" | ||
}; | ||
} | ||
|
||
const view: DataView = new DataView(socks5DataBuffer); | ||
const cmd: number = view.getUint8(0); | ||
if (cmd !== 1) { | ||
return { | ||
hasError: true, | ||
message: "unsupported command, only TCP (CONNECT) is allowed" | ||
}; | ||
} | ||
|
||
const atype: number = view.getUint8(1); | ||
let addressLength: number = 0; | ||
let addressIndex: number = 2; | ||
let address: string = ""; | ||
switch (atype) { | ||
case 1: | ||
addressLength = 4; | ||
address = new Uint8Array( | ||
socks5DataBuffer.slice(addressIndex, addressIndex + addressLength) | ||
).join("."); | ||
break; | ||
case 3: | ||
addressLength = new Uint8Array( | ||
socks5DataBuffer.slice(addressIndex, addressIndex + 1) | ||
)[0]; | ||
addressIndex += 1; | ||
address = new TextDecoder().decode( | ||
socks5DataBuffer.slice(addressIndex, addressIndex + addressLength) | ||
); | ||
break; | ||
case 4: | ||
addressLength = 16; | ||
const dataView = new DataView(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); | ||
const ipv6 = []; | ||
for (let i = 0; i < 8; i++) { | ||
ipv6.push(dataView.getUint16(i * 2).toString(16)); | ||
} | ||
address = ipv6.join(":"); | ||
break; | ||
default: | ||
return { | ||
hasError: true, | ||
message: `invalid addressType is ${atype}` | ||
}; | ||
} | ||
|
||
if (!address) { | ||
return { | ||
hasError: true, | ||
message: `address is empty, addressType is ${atype}` | ||
}; | ||
} | ||
|
||
const portIndex: number = addressIndex + addressLength; | ||
const portBuffer: ArrayBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2); | ||
const portRemote: number = new DataView(portBuffer).getUint16(0); | ||
return { | ||
hasError: false, | ||
addressRemote: address, | ||
portRemote, | ||
rawDataIndex: addressIndex + addressLength, | ||
}; | ||
} | ||
|
||
async function HandleTCPOutbound(remoteSocket: RemoteSocketWrapper, addressRemote: string, portRemote: number, rawClientData: Uint8Array | undefined, webSocket: WebSocket, env: Env): Promise<void> { | ||
const maxRetryCount = 5 | ||
let retryCount = 0; | ||
|
||
async function connectAndWrite(address: string, port: number) { | ||
const socketAddress: SocketAddress = { | ||
hostname: address, | ||
port: port, | ||
} | ||
const tcpSocket: Socket = connect(socketAddress) | ||
remoteSocket.value = tcpSocket | ||
const writer: WritableStreamDefaultWriter<Uint8Array> = tcpSocket.writable.getWriter() | ||
await writer.write(rawClientData) | ||
writer.releaseLock() | ||
return tcpSocket | ||
} | ||
|
||
async function retry() { | ||
retryCount++ | ||
if (retryCount > maxRetryCount) { | ||
return | ||
} | ||
|
||
if (!proxyList.length) { | ||
countries = (await env.settings.get("Countries"))?.split(",").filter(t => t.trim().length > 0) || [] | ||
proxyList = await fetch("https://raw.githubusercontent.com/vfarid/v2ray-worker/main/resources/proxy-list.txt").then(r => r.text()).then(t => t.trim().split("\n").filter(t => t.trim().length > 0)) | ||
if (countries.length > 0) { | ||
proxyList = proxyList.filter(t => { | ||
const arr = t.split(",") | ||
if (arr.length > 0) { | ||
return countries.includes(arr[1]) | ||
} | ||
}) | ||
} | ||
proxyList = proxyList.map(ip => ip.split(",")[0]) | ||
console.log(proxyList) | ||
} | ||
if (proxyList.length > 0) { | ||
proxyIP = proxyList[Math.floor(Math.random() * proxyList.length)] | ||
const tcpSocket: Socket = await connectAndWrite(proxyIP, portRemote) | ||
RemoteSocketToWS(tcpSocket, webSocket, retry) | ||
} | ||
} | ||
|
||
const tcpSocket: Socket = await connectAndWrite(addressRemote, portRemote) | ||
RemoteSocketToWS(tcpSocket, webSocket, retry) | ||
} | ||
|
||
function MakeReadableWebSocketStream(webSocketServer: WebSocket, earlyDataHeader: string): ReadableStream { | ||
let readableStreamCancel = false; | ||
const stream = new ReadableStream({ | ||
start(controller) { | ||
webSocketServer.addEventListener("message", (event) => { | ||
if (readableStreamCancel) { | ||
return; | ||
} | ||
const message = event.data; | ||
controller.enqueue(message); | ||
}); | ||
webSocketServer.addEventListener("close", () => { | ||
SafeCloseWebSocket(webSocketServer); | ||
if (readableStreamCancel) { | ||
return; | ||
} | ||
controller.close(); | ||
}); | ||
webSocketServer.addEventListener("error", (err) => { | ||
controller.error(err); | ||
}); | ||
const { earlyData, error } = Base64ToArrayBuffer(earlyDataHeader); | ||
if (error) { | ||
controller.error(error); | ||
} else if (earlyData) { | ||
controller.enqueue(earlyData); | ||
} | ||
}, | ||
pull(controller) {}, | ||
cancel(reason) { | ||
if (readableStreamCancel) { | ||
return; | ||
} | ||
readableStreamCancel = true; | ||
SafeCloseWebSocket(webSocketServer); | ||
} | ||
}); | ||
return stream; | ||
} | ||
|
||
async function RemoteSocketToWS(remoteSocket: Socket, webSocket: WebSocket, retry: (() => Promise<void>) | null): Promise<void> { | ||
let hasIncomingData: boolean = false | ||
await remoteSocket.readable | ||
.pipeTo( | ||
new WritableStream({ | ||
async write(chunk: Uint8Array, controller: WritableStreamDefaultController) { | ||
try { | ||
hasIncomingData = true | ||
if (webSocket.readyState !== WS_READY_STATE_OPEN) { | ||
controller.error("webSocket.readyState is not open, maybe close") | ||
} | ||
webSocket.send(chunk) | ||
} catch (e) { } | ||
}, | ||
abort(reason: any) { | ||
// console.error("remoteConnection!.readable abort", reason) | ||
}, | ||
}) | ||
) | ||
.catch((error) => { | ||
// console.error("RemoteSocketToWS has exception ", error.stack || error) | ||
SafeCloseWebSocket(webSocket) | ||
}) | ||
|
||
if (hasIncomingData === false && retry) { | ||
retry() | ||
} | ||
} | ||
|
||
function IsValidSHA224(hash: string): boolean { | ||
const sha224Regex = /^[0-9a-f]{56}$/i; | ||
return sha224Regex.test(hash); | ||
} | ||
|
||
function Base64ToArrayBuffer(base64Str: string): CustomArrayBuffer { | ||
if (!base64Str) { | ||
return { | ||
earlyData: null, | ||
error: null | ||
} | ||
} | ||
try { | ||
base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/') | ||
const decode: string = atob(base64Str) | ||
const arryBuffer: Uint8Array = Uint8Array.from(decode, (c) => c.charCodeAt(0)) | ||
return { | ||
earlyData: arryBuffer.buffer, | ||
error: null | ||
} | ||
} catch (error) { | ||
return { | ||
earlyData: null, | ||
error | ||
} | ||
} | ||
} | ||
|
||
function SafeCloseWebSocket(socket: WebSocket): void { | ||
try { | ||
if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { | ||
socket.close() | ||
} | ||
} catch (error) { } | ||
} |