feature: Implement WebSocket backend for passyfire.
Co-authored-by: dess <devessa@users.noreply.github.com>
This commit is contained in:
parent
59f66cbe5b
commit
535b8cde9a
5 changed files with 142 additions and 5 deletions
10
api/package-lock.json
generated
10
api/package-lock.json
generated
|
@ -19,6 +19,7 @@
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"@types/ssh2": "^1.15.0",
|
"@types/ssh2": "^1.15.0",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
"nodemon": "^3.0.3",
|
"nodemon": "^3.0.3",
|
||||||
"prisma": "^5.13.0",
|
"prisma": "^5.13.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
@ -196,6 +197,15 @@
|
||||||
"undici-types": "~5.26.4"
|
"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": {
|
"node_modules/abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"@types/ssh2": "^1.15.0",
|
"@types/ssh2": "^1.15.0",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
"nodemon": "^3.0.3",
|
"nodemon": "^3.0.3",
|
||||||
"prisma": "^5.13.0",
|
"prisma": "^5.13.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
|
|
@ -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 type { ForwardRule, ConnectedClient, ParameterReturnedValue, BackendBaseClass } from "../base.js";
|
||||||
import { route } from "./routes.js";
|
|
||||||
import { generateRandomData } from "../../libs/generateRandom.js";
|
import { generateRandomData } from "../../libs/generateRandom.js";
|
||||||
|
import { requestHandler } from "./socket.js";
|
||||||
|
import { route } from "./routes.js";
|
||||||
|
|
||||||
type BackendProviderUser = {
|
type BackendProviderUser = {
|
||||||
username: string,
|
username: string,
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardRuleExt = ForwardRule & {
|
export type ForwardRuleExt = ForwardRule & {
|
||||||
protocol: "tcp" | "udp",
|
protocol: "tcp" | "udp",
|
||||||
userConfig: Record<string, string>
|
userConfig: Record<string, string>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ConnectedClientExt = ConnectedClient & {
|
||||||
|
connectionDetails: ForwardRuleExt;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Fight me (for better naming)
|
// Fight me (for better naming)
|
||||||
type BackendParsedProviderString = {
|
type BackendParsedProviderString = {
|
||||||
ip: string,
|
ip: string,
|
||||||
|
@ -67,7 +76,7 @@ function parseBackendProviderString(data: string): BackendParsedProviderString {
|
||||||
export class PassyFireBackendProvider implements BackendBaseClass {
|
export class PassyFireBackendProvider implements BackendBaseClass {
|
||||||
state: "stopped" | "stopping" | "started" | "starting";
|
state: "stopped" | "stopping" | "started" | "starting";
|
||||||
|
|
||||||
clients: ConnectedClient[];
|
clients: ConnectedClientExt[];
|
||||||
proxies: ForwardRuleExt[];
|
proxies: ForwardRuleExt[];
|
||||||
users: LoggedInUser[];
|
users: LoggedInUser[];
|
||||||
logs: string[];
|
logs: string[];
|
||||||
|
@ -94,8 +103,11 @@ export class PassyFireBackendProvider implements BackendBaseClass {
|
||||||
trustProxy: this.options.isProxied
|
trustProxy: this.options.isProxied
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.fastify.register(fastifyWebsocket);
|
||||||
route(this);
|
route(this);
|
||||||
|
|
||||||
|
this.fastify.get("/", { websocket: true }, (ws, req) => requestHandler(this, ws, req));
|
||||||
|
|
||||||
await this.fastify.listen({
|
await this.fastify.listen({
|
||||||
port: this.options.port,
|
port: this.options.port,
|
||||||
host: this.options.ip
|
host: this.options.ip
|
||||||
|
|
114
api/src/backendimpl/passyfire-reimpl/socket.ts
Normal file
114
api/src/backendimpl/passyfire-reimpl/socket.ts
Normal file
|
@ -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}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue