Skip to content

Commit

Permalink
feature: Implement WebSocket backend for passyfire.
Browse files Browse the repository at this point in the history
Co-authored-by: dess <[email protected]>
  • Loading branch information
imterah and dess committed May 5, 2024
1 parent 59f66cb commit 535b8cd
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 5 deletions.
10 changes: 10 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 16 additions & 4 deletions api/src/backendimpl/passyfire-reimpl/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>
};

export type ConnectedClientExt = ConnectedClient & {
connectionDetails: ForwardRuleExt;
username: string;
};

// Fight me (for better naming)
type BackendParsedProviderString = {
ip: string,
Expand Down Expand Up @@ -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[];
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion api/src/backendimpl/passyfire-reimpl/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down
114 changes: 114 additions & 0 deletions api/src/backendimpl/passyfire-reimpl/socket.ts
Original file line number Diff line number Diff line change
@@ -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}'`);
}
});
}

0 comments on commit 535b8cd

Please sign in to comment.