This commit is contained in:
Holly Stubbs 2024-01-18 14:48:09 +00:00
parent 4480165793
commit 1798cdbc5f
14 changed files with 1982 additions and 1 deletions

4
.gitignore vendored
View file

@ -1,3 +1,7 @@
# Custom
testing/
build/
# ---> Node
# Logs
logs

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 tgpholly
Copyright (c) 2024 Holly Stubbs (tgpholly)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -0,0 +1,5 @@
{
"remoteAddress": "ws://localhost:18472",
"localPort": 25566,
"authKey": "default"
}

View file

@ -0,0 +1,6 @@
{
"port": 18472,
"localHost": "localhost",
"localPort": 25565,
"authKey": "default"
}

197
index.ts Normal file
View file

@ -0,0 +1,197 @@
import { WebSocket, WebSocketServer } from "ws";
import { Socket, createServer } from "net";
import { Packet } from "./src/enum/Packet";
import { Endian, createReader, createWriter } from "bufferstuff";
import { existsSync, readFileSync } from "fs";
import IServerConfig from "./src/interface/IServerConfig";
import { AuthState } from "./src/enum/AuthState";
import IClientConfig from "./src/interface/IClientConfig";
const keepAliveMessage = createWriter(Endian.LE, 1).writeUByte(Packet.KeepAlive).toBuffer();
if (process.argv[2] === "server") {
if (!existsSync("./config/server-config.json")) {
console.error("server-config.json is missing!");
process.exit(1);
}
const config:IServerConfig = JSON.parse(readFileSync("./config/server-config.json").toString());
const server = new WebSocketServer({ port: config.port }, () => console.log(`Server started at ${config.port}`));
server.on("connection", (socket) => {
console.log("Connection");
let queuedMessages = new Array<Buffer>();
let connectedToServer = false;
let connectingToServer = false;
let authed = false;
let client:Socket | null = null;
const clientKeepAlive = setInterval(() => {
socket.send(keepAliveMessage);
}, 5000);
socket.on("message", (data, isBinary) => {
if (!isBinary) {
return;
}
// NOTE: The types declarations for ws are really messed up >:(
// @ts-ignore
const packetData = createReader(Endian.LE, data);
const packetId = packetData.readUByte();
let tempReadLength = 0;
switch (packetId) {
case Packet.KeepAlive: break; // We don't really care, it's just so CF doesn't drop the connection.
case Packet.Auth:
try {
if (packetData.readShortString() !== config.authKey) {
socket.send(createWriter(Endian.LE, 2).writeUByte(Packet.Auth).writeUByte(AuthState.Bad).toBuffer());
socket.close();
return;
}
socket.send(createWriter(Endian.LE, 2).writeUByte(Packet.Auth).writeUByte(AuthState.Good).toBuffer());
authed = true;
client = new Socket();
client.on("connect", () => {
connectedToServer = true;
if (queuedMessages.length > 0) {
for (const message of queuedMessages) {
console.log("Sent", message);
client?.write(message);
}
queuedMessages.length = 0;
}
});
client.on("data", (chunk) => {
socket.send(createWriter(Endian.LE, 5)
.writeUByte(Packet.Data)
.writeUInt(chunk.length)
.writeBuffer(chunk)
.toBuffer());
});
function clientCloseOrError() {
socket.close();
}
client.on("close", clientCloseOrError);
client.on("error", clientCloseOrError);
connectingToServer = true;
client.connect({
host: config.localHost,
port: config.localPort
});
} catch (e) {
client?.end();
client = null;
socket.close();
}
break;
case Packet.Data:
if (!authed) {
return;
}
tempReadLength = packetData.readUInt();
if (connectedToServer) {
client?.write(packetData.readBuffer(tempReadLength));
} else {
queuedMessages.push(packetData.readBuffer(tempReadLength));
}
//console.log("[SERVER] Data:", data, " Length:", tempReadLength);
break;
}
});
function closeOrError() {
clearInterval(clientKeepAlive);
client?.end();
}
socket.on("close", closeOrError);
socket.on("error", closeOrError);
});
} else if (process.argv[2] === "client") {
if (!existsSync("./config/client-config.json")) {
console.error("client-config.json is missing!");
process.exit(1);
}
const config:IClientConfig = JSON.parse(readFileSync("./config/client-config.json").toString());
const server = createServer((socket) => {
let authed = false;
let queuedMessages = new Array<Buffer>();
let txBytes = 0;
let rxBytes = 0;
const txrxInterval = setInterval(() => {
console.log(`TX: ${(txBytes / 1024).toFixed(2)}KB/s | RX: ${(rxBytes / 1024).toFixed(2)}KB/s`);
txBytes = rxBytes = 0;
}, 1000);
const client = new WebSocket(config.remoteAddress);
client.on("open", () => {
// Send off auth as soon as we connect
client.send(createWriter(Endian.LE, 2 + config.authKey.length).writeUByte(Packet.Auth).writeShortString(config.authKey).toBuffer());
});
client.on("message", (data) => {
// @ts-ignore
const packetData = createReader(Endian.LE, data);
const packetId = packetData.readUByte();
let tempReadLength = 0;
let bufferData:Buffer;
switch (packetId) {
case Packet.KeepAlive:
client.send(keepAliveMessage);
break;
case Packet.Auth:
if (authed) {
return;
}
authed = packetData.readUByte() === AuthState.Good;
if (authed && queuedMessages.length > 0) {
for (const message of queuedMessages) {
client.send(message);
}
queuedMessages.length = 0;
}
break;
case Packet.Data:
tempReadLength = packetData.readUInt();
bufferData = packetData.readBuffer(tempReadLength);
rxBytes += bufferData.length;
socket.write(bufferData);
break;
}
});
function clientCloseOrError() {
socket.end();
clearInterval(txrxInterval);
}
client.on("close", clientCloseOrError);
client.on("error", clientCloseOrError);
socket.on("data", (chunk) => {
const bufferData = createWriter(Endian.LE, 5).writeUByte(Packet.Data).writeUInt(chunk.length).writeBuffer(chunk).toBuffer();
txBytes += chunk.length;
if (authed) {
client.send(bufferData);
} else {
queuedMessages.push(bufferData);
}
});
function serverCloseOrError() {
client.close();
clearInterval(txrxInterval);
}
socket.on("close", serverCloseOrError);
socket.on("error", serverCloseOrError);
});
server.listen(config.localPort, () => console.log(`Local server listening at ${config.localPort}`));
}

1670
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "tcp-ws-proxy",
"version": "1.0.0",
"description": "A TCP -> WS -> TCP proxy",
"main": "build/index.js",
"scripts": {
"dev:updateCheck": "check-outdated",
"dev:run": "nodemon --watch './**/*.ts' --watch './**/*.json' --exec ts-node index.ts",
"build": "npm-run-all build:*",
"build:build": "ncc build index.ts -o build",
"build:mangle": "ts-node ./tooling/mangle.ts",
"build:cleanup": "ts-node ./tooling/cleanup.ts",
"_clean": "tsc --build --clean"
},
"repository": {
"type": "git",
"url": "https://git.eusv.net/tgpholly/tcp-ws-proxy.git"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.11.5",
"@types/ws": "^8.5.10",
"@vercel/ncc": "^0.38.1",
"check-outdated": "^2.12.0",
"npm-run-all": "^4.1.5",
"terser": "^5.27.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"bufferstuff": "^1.5.0",
"dyetty": "^1.0.1",
"ws": "^8.16.0"
}
}

4
src/enum/AuthState.ts Normal file
View file

@ -0,0 +1,4 @@
export enum AuthState {
Bad,
Good
}

5
src/enum/Packet.ts Normal file
View file

@ -0,0 +1,5 @@
export enum Packet {
KeepAlive,
Auth,
Data
}

View file

@ -0,0 +1,5 @@
export default interface IClientConfig {
remoteAddress: string,
localPort: number,
authKey: string
}

View file

@ -0,0 +1,6 @@
export default interface IServerConfig {
port: number,
localHost: string,
localPort: number,
authKey: string
}

11
tooling/cleanup.ts Normal file
View file

@ -0,0 +1,11 @@
import { readdirSync, rmSync, readFileSync } from "fs";
const libFiles = readdirSync("./build");
const mangled = readFileSync("./build/.MANGLED").toString() === "false";
for (const file of libFiles) {
if (!file.startsWith(mangled ? "index.min.js" : "index.js")) {
rmSync(`./build/${file}`, { recursive: true });
}
}

18
tooling/mangle.ts Normal file
View file

@ -0,0 +1,18 @@
import { readFileSync, writeFileSync } from "fs";
import { minify } from "terser";
const DISABLE = false;
writeFileSync("./build/.MANGLED", `${DISABLE}`);
if (DISABLE) {
//writeFileSync("./build/index.js", readFileSync("./build/index.js"));
console.warn("[WARNING] mangle.ts is disabled!");
} else {
(async () => {
const mangled = await minify(readFileSync("./build/index.js").toString(), {
mangle: true,
toplevel: true,
});
writeFileSync("./build/index.min.js", `${mangled.code}`);
})();
}

13
tsconfig.json Normal file
View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node",
"target": "ES2020",
"esModuleInterop": true,
"resolveJsonModule": true,
"rootDir": "./",
"outDir": "./build/",
"strict": true,
"declaration": true
}
}