From 535b8cde9a79b6364fa811b7fd3e622645237d95 Mon Sep 17 00:00:00 2001 From: greysoh Date: Sun, 5 May 2024 16:43:57 -0400 Subject: [PATCH] feature: Implement WebSocket backend for passyfire. Co-authored-by: dess --- api/package-lock.json | 10 ++ api/package.json | 1 + api/src/backendimpl/passyfire-reimpl/index.ts | 20 ++- .../backendimpl/passyfire-reimpl/routes.ts | 2 +- .../backendimpl/passyfire-reimpl/socket.ts | 114 ++++++++++++++++++ 5 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 api/src/backendimpl/passyfire-reimpl/socket.ts diff --git a/api/package-lock.json b/api/package-lock.json index 2048d84..f20bcaa 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -19,6 +19,7 @@ "@types/bcrypt": "^5.0.2", "@types/node": "^20.12.7", "@types/ssh2": "^1.15.0", + "@types/ws": "^8.5.10", "nodemon": "^3.0.3", "prisma": "^5.13.0", "typescript": "^5.3.3" @@ -196,6 +197,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", diff --git a/api/package.json b/api/package.json index 2c540bb..395e32e 100644 --- a/api/package.json +++ b/api/package.json @@ -17,6 +17,7 @@ "@types/bcrypt": "^5.0.2", "@types/node": "^20.12.7", "@types/ssh2": "^1.15.0", + "@types/ws": "^8.5.10", "nodemon": "^3.0.3", "prisma": "^5.13.0", "typescript": "^5.3.3" diff --git a/api/src/backendimpl/passyfire-reimpl/index.ts b/api/src/backendimpl/passyfire-reimpl/index.ts index b4a5d48..0da49cd 100644 --- a/api/src/backendimpl/passyfire-reimpl/index.ts +++ b/api/src/backendimpl/passyfire-reimpl/index.ts @@ -1,19 +1,28 @@ -import Fastify, { type FastifyInstance } from "fastify"; +import fastifyWebsocket from "@fastify/websocket"; + +import type { FastifyInstance } from "fastify"; +import Fastify from "fastify"; import type { ForwardRule, ConnectedClient, ParameterReturnedValue, BackendBaseClass } from "../base.js"; -import { route } from "./routes.js"; import { generateRandomData } from "../../libs/generateRandom.js"; +import { requestHandler } from "./socket.js"; +import { route } from "./routes.js"; type BackendProviderUser = { username: string, password: string } -type ForwardRuleExt = ForwardRule & { +export type ForwardRuleExt = ForwardRule & { protocol: "tcp" | "udp", userConfig: Record }; +export type ConnectedClientExt = ConnectedClient & { + connectionDetails: ForwardRuleExt; + username: string; +}; + // Fight me (for better naming) type BackendParsedProviderString = { ip: string, @@ -67,7 +76,7 @@ function parseBackendProviderString(data: string): BackendParsedProviderString { export class PassyFireBackendProvider implements BackendBaseClass { state: "stopped" | "stopping" | "started" | "starting"; - clients: ConnectedClient[]; + clients: ConnectedClientExt[]; proxies: ForwardRuleExt[]; users: LoggedInUser[]; logs: string[]; @@ -94,8 +103,11 @@ export class PassyFireBackendProvider implements BackendBaseClass { trustProxy: this.options.isProxied }); + await this.fastify.register(fastifyWebsocket); route(this); + this.fastify.get("/", { websocket: true }, (ws, req) => requestHandler(this, ws, req)); + await this.fastify.listen({ port: this.options.port, host: this.options.ip diff --git a/api/src/backendimpl/passyfire-reimpl/routes.ts b/api/src/backendimpl/passyfire-reimpl/routes.ts index 5707c17..f1cb560 100644 --- a/api/src/backendimpl/passyfire-reimpl/routes.ts +++ b/api/src/backendimpl/passyfire-reimpl/routes.ts @@ -3,7 +3,7 @@ import type { PassyFireBackendProvider } from "./index.js"; export function route(instance: PassyFireBackendProvider) { const { fastify } = instance; - + const proxiedPort: number = instance.options.publicPort ?? 443; const unsupportedSpoofedRoutes: string[] = [ diff --git a/api/src/backendimpl/passyfire-reimpl/socket.ts b/api/src/backendimpl/passyfire-reimpl/socket.ts new file mode 100644 index 0000000..bd97620 --- /dev/null +++ b/api/src/backendimpl/passyfire-reimpl/socket.ts @@ -0,0 +1,114 @@ +import dgram from "node:dgram"; +import net from "node:net"; + +import type { WebSocket } from "@fastify/websocket"; +import type { FastifyRequest } from "fastify"; + +import type { ConnectedClientExt, PassyFireBackendProvider } from "./index.js"; + +// This code sucks because this protocol sucks BUUUT it works, and I don't wanna reinvent +// the gosh darn wheel for (almost) no reason + +function authenticateSocket(instance: PassyFireBackendProvider, ws: WebSocket, message: string, state: ConnectedClientExt): Boolean { + if (!message.startsWith("Accept: ")) { + ws.send("400 Bad Request"); + return false; + } + + const type = message.substring(message.indexOf(":") + 1).trim(); + + if (type == "IsPassedWS") { + ws.send("AcceptResponse IsPassedWS: true"); + } else if (type.startsWith("Bearer")) { + const token = type.substring(type.indexOf("Bearer") + 7); + + for (const proxy of instance.proxies) { + for (const username of Object.keys(proxy.userConfig)) { + const currentToken = proxy.userConfig[username]; + + if (token == currentToken) { + state.connectionDetails = proxy; + state.username = username; + }; + }; + }; + + if (state.connectionDetails && state.username) { + ws.send("AcceptResponse Bearer: true"); + return true; + } else { + ws.send("AcceptResponse Bearer: false"); + } + } + + return false; +} + +export function requestHandler(instance: PassyFireBackendProvider, ws: WebSocket, req: FastifyRequest) { + let state: "authentication" | "data" = "authentication"; + let socket: dgram.Socket | net.Socket | undefined; + + // @ts-ignore + let connectedClient: ConnectedClientExt = {}; + + ws.on("close", () => { + instance.clients.splice(instance.clients.indexOf(connectedClient as ConnectedClientExt), 1); + }); + + ws.on("message", (rawData: ArrayBuffer) => { + if (state == "authentication") { + const data = rawData.toString(); + + if (authenticateSocket(instance, ws, data, connectedClient)) { + ws.send("AcceptResponse Bearer: true"); + + connectedClient.ip = req.ip; + connectedClient.port = req.socket.remotePort ?? -1; + + instance.clients.push(connectedClient); + + if (connectedClient.connectionDetails.protocol == "tcp") { + socket = new net.Socket(); + + socket.connect(connectedClient.connectionDetails.sourcePort, connectedClient.connectionDetails.sourceIP); + + socket.on("connect", () => { + state = "data"; + + ws.send("InitProxy: Attempting to connect"); + ws.send("InitProxy: Connected"); + }); + + socket.on("data", (data) => { + ws.send(data); + }); + } else if (connectedClient.connectionDetails.protocol == "udp") { + socket = dgram.createSocket("udp4"); + state = "data"; + + ws.send("InitProxy: Attempting to connect"); + ws.send("InitProxy: Connected"); + + socket.on("message", (data, rinfo) => { + if (rinfo.address != connectedClient.connectionDetails.sourceIP || rinfo.port != connectedClient.connectionDetails.sourcePort) return; + ws.send(data); + }); + } + } + } else if (state == "data") { + if (socket instanceof dgram.Socket) { + const array = new Uint8Array(rawData); + + socket.send(array, connectedClient.connectionDetails.sourcePort, connectedClient.connectionDetails.sourceIP, (err) => { + if (err) throw err; + }); + } else if (socket instanceof net.Socket) { + const array = new Uint8Array(rawData); + + socket.write(array); + } + } else { + throw new Error(`Whooops, our WebSocket reached an unsupported state: '${state}'`); + } + }); +} \ No newline at end of file