diff --git a/api/src/backendimpl/index.ts b/api/src/backendimpl/index.ts index b248aff..98f6e28 100644 --- a/api/src/backendimpl/index.ts +++ b/api/src/backendimpl/index.ts @@ -4,6 +4,6 @@ import { PassyFireBackendProvider } from "./passyfire-reimpl/index.js"; import { SSHBackendProvider } from "./ssh.js"; export const backendProviders: Record = { - "ssh": SSHBackendProvider, - "passyfire": PassyFireBackendProvider -}; \ No newline at end of file + ssh: SSHBackendProvider, + passyfire: PassyFireBackendProvider, +}; diff --git a/api/src/backendimpl/passyfire-reimpl/index.ts b/api/src/backendimpl/passyfire-reimpl/index.ts index 0da49cd..5ffd7f2 100644 --- a/api/src/backendimpl/passyfire-reimpl/index.ts +++ b/api/src/backendimpl/passyfire-reimpl/index.ts @@ -3,40 +3,45 @@ 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 { generateRandomData } from "../../libs/generateRandom.js"; import { requestHandler } from "./socket.js"; import { route } from "./routes.js"; type BackendProviderUser = { - username: string, - password: string -} + username: string; + password: string; +}; export type ForwardRuleExt = ForwardRule & { - protocol: "tcp" | "udp", - userConfig: Record + protocol: "tcp" | "udp"; + userConfig: Record; }; export type ConnectedClientExt = ConnectedClient & { connectionDetails: ForwardRuleExt; - username: string; + username: string; }; // Fight me (for better naming) type BackendParsedProviderString = { - ip: string, - port: number, - publicPort?: number, - isProxied?: boolean, + ip: string; + port: number; + publicPort?: number; + isProxied?: boolean; - users: BackendProviderUser[] -} + users: BackendProviderUser[]; +}; type LoggedInUser = { - username: string, - token: string -} + username: string; + token: string; +}; function parseBackendProviderString(data: string): BackendParsedProviderString { try { @@ -47,19 +52,30 @@ function parseBackendProviderString(data: string): BackendParsedProviderString { const jsonData = JSON.parse(data); - if (typeof jsonData.ip != "string") throw new Error("IP field is not a string"); + if (typeof jsonData.ip != "string") + throw new Error("IP field is not a string"); if (typeof jsonData.port != "number") throw new Error("Port is not a number"); - if (typeof jsonData.publicPort != "undefined" && typeof jsonData.publicPort != "number") throw new Error("(optional field) Proxied port is not a number"); - if (typeof jsonData.isProxied != "undefined" && typeof jsonData.isProxied != "boolean") throw new Error("(optional field) 'Is proxied' is not a boolean"); + if ( + typeof jsonData.publicPort != "undefined" && + typeof jsonData.publicPort != "number" + ) + throw new Error("(optional field) Proxied port is not a number"); + if ( + typeof jsonData.isProxied != "undefined" && + typeof jsonData.isProxied != "boolean" + ) + throw new Error("(optional field) 'Is proxied' is not a boolean"); if (!Array.isArray(jsonData.users)) throw new Error("Users is not an array"); for (const userIndex in jsonData.users) { const user = jsonData.users[userIndex]; - - if (typeof user.username != "string") throw new Error("Username is not a string, in users array"); - if (typeof user.password != "string") throw new Error("Password is not a string, in users array"); + + if (typeof user.username != "string") + throw new Error("Username is not a string, in users array"); + if (typeof user.password != "string") + throw new Error("Password is not a string, in users array"); } return { @@ -69,8 +85,8 @@ function parseBackendProviderString(data: string): BackendParsedProviderString { publicPort: jsonData.publicPort, isProxied: jsonData.isProxied, - users: jsonData.users - } + users: jsonData.users, + }; } export class PassyFireBackendProvider implements BackendBaseClass { @@ -87,8 +103,8 @@ export class PassyFireBackendProvider implements BackendBaseClass { constructor(parameters: string) { this.logs = []; this.clients = []; - this.proxies = []; - + this.proxies = []; + this.state = "stopped"; this.options = parseBackendProviderString(parameters); @@ -100,17 +116,19 @@ export class PassyFireBackendProvider implements BackendBaseClass { this.fastify = Fastify({ logger: true, - trustProxy: this.options.isProxied + trustProxy: this.options.isProxied, }); await this.fastify.register(fastifyWebsocket); route(this); - this.fastify.get("/", { websocket: true }, (ws, req) => requestHandler(this, ws, req)); + this.fastify.get("/", { websocket: true }, (ws, req) => + requestHandler(this, ws, req), + ); await this.fastify.listen({ port: this.options.port, - host: this.options.ip + host: this.options.ip, }); this.state = "started"; @@ -124,18 +142,23 @@ export class PassyFireBackendProvider implements BackendBaseClass { this.users.splice(0, this.users.length); this.proxies.splice(0, this.proxies.length); this.clients.splice(0, this.clients.length); - - return true; - }; - addConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void { + return true; + } + + addConnection( + sourceIP: string, + sourcePort: number, + destPort: number, + protocol: "tcp" | "udp", + ): void { const proxy: ForwardRuleExt = { sourceIP, sourcePort, destPort, protocol, - userConfig: {} + userConfig: {}, }; for (const user of this.options.users) { @@ -143,29 +166,49 @@ export class PassyFireBackendProvider implements BackendBaseClass { } this.proxies.push(proxy); - }; + } - removeConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void { - const connectionCheck = PassyFireBackendProvider.checkParametersConnection(sourceIP, sourcePort, destPort, protocol); + removeConnection( + sourceIP: string, + sourcePort: number, + destPort: number, + protocol: "tcp" | "udp", + ): void { + const connectionCheck = PassyFireBackendProvider.checkParametersConnection( + sourceIP, + sourcePort, + destPort, + protocol, + ); if (!connectionCheck.success) throw new Error(connectionCheck.message); - const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort); + const foundProxyEntry = this.proxies.find( + i => + i.sourceIP == sourceIP && + i.sourcePort == sourcePort && + i.destPort == destPort, + ); if (!foundProxyEntry) return; this.proxies.splice(this.proxies.indexOf(foundProxyEntry), 1); return; - }; + } getAllConnections(): ConnectedClient[] { if (this.clients == null) return []; return this.clients; - }; + } - static checkParametersConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): ParameterReturnedValue { + static checkParametersConnection( + sourceIP: string, + sourcePort: number, + destPort: number, + protocol: "tcp" | "udp", + ): ParameterReturnedValue { return { - success: true - } - }; + success: true, + }; + } static checkParametersBackendInstance(data: string): ParameterReturnedValue { try { @@ -174,12 +217,12 @@ export class PassyFireBackendProvider implements BackendBaseClass { } catch (e: Error) { return { success: false, - message: e.toString() - } + message: e.toString(), + }; } return { - success: true - } - }; -} \ No newline at end of file + success: true, + }; + } +} diff --git a/api/src/backendimpl/passyfire-reimpl/routes.ts b/api/src/backendimpl/passyfire-reimpl/routes.ts index f1cb560..2961483 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[] = [ @@ -16,7 +16,7 @@ export function route(instance: PassyFireBackendProvider) { "/api/v1/tunnels/stop", // Same scenario for this API. - "/api/v1/users", + "/api/v1/users", "/api/v1/users/add", "/api/v1/users/remove", "/api/v1/users/enable", @@ -31,7 +31,7 @@ export function route(instance: PassyFireBackendProvider) { add: true, remove: true, get: true, - getPasswords: true + getPasswords: true, }, routes: { add: true, @@ -39,125 +39,140 @@ export function route(instance: PassyFireBackendProvider) { start: true, stop: true, get: true, - getPasswords: true - } - } - } + getPasswords: true, + }, + }, + }; }); for (const spoofedRoute of unsupportedSpoofedRoutes) { fastify.post(spoofedRoute, (req, res) => { - if (typeof req.body != "string") return res.status(400).send({ - error: "Invalid token" - }); + if (typeof req.body != "string") + return res.status(400).send({ + error: "Invalid token", + }); try { JSON.parse(req.body); } catch (e) { return res.status(400).send({ - error: "Invalid token" - }) + error: "Invalid token", + }); } // @ts-ignore - if (!req.body.token) return res.status(400).send({ - error: "Invalid token" - }); + if (!req.body.token) + return res.status(400).send({ + error: "Invalid token", + }); return res.status(403).send({ - error: "Invalid scope(s)" + error: "Invalid scope(s)", }); - }) + }); } - fastify.post("/api/v1/users/login", { - schema: { - body: { - type: "object", - required: ["username", "password"], + fastify.post( + "/api/v1/users/login", + { + schema: { + body: { + type: "object", + required: ["username", "password"], - properties: { - username: { type: "string" }, - password: { type: "string" } - } - } - } - }, (req, res) => { - // @ts-ignore - const body: { - username: string, - password: string - } = req.body; - - if (!instance.options.users.find((i) => i.username == body.username && i.password == body.password)) { - return res.status(403).send({ - error: "Invalid username/password." - }); - }; - - const token = generateRandomData(); - - instance.users.push({ - username: body.username, - token - }); - - return { - success: true, - data: { - token - } - } - }); - - fastify.post("/api/v1/tunnels", { - schema: { - body: { - type: "object", - required: ["token"], - properties: { - token: { type: "string" }, + properties: { + username: { type: "string" }, + password: { type: "string" }, + }, }, }, }, - }, async (req, res) => { - // @ts-ignore - const body: { - token: string - } = req.body; + (req, res) => { + // @ts-ignore + const body: { + username: string; + password: string; + } = req.body; - const userData = instance.users.find(user => user.token == body.token); + if ( + !instance.options.users.find( + i => i.username == body.username && i.password == body.password, + ) + ) { + return res.status(403).send({ + error: "Invalid username/password.", + }); + } - if (!userData) return res.status(403).send({ - error: "Invalid token" - }); + const token = generateRandomData(); - // const host = req.hostname.substring(0, req.hostname.indexOf(":")); - const unparsedPort = req.hostname.substring(req.hostname.indexOf(":") + 1); - - // @ts-ignore - // parseInt(...) can take a number just fine, at least in Node.JS - const port = parseInt(unparsedPort == "" ? proxiedPort : unparsedPort); + instance.users.push({ + username: body.username, + token, + }); - // This protocol is so confusing. I'm sorry. - res.send({ - success: true, - data: instance.proxies.map((proxy) => ({ - proxyUrlSettings: { - host: "sameAs", // Makes pfC work (this is by design apparently) - port, - protocol: proxy.protocol.toUpperCase() + return { + success: true, + data: { + token, }, - - dest: `${proxy.sourceIP}:${proxy.destPort}`, - name: `${proxy.protocol.toUpperCase()} on ::${proxy.sourcePort} -> ::${proxy.destPort}`, - - passwords: [ - proxy.userConfig[userData.username] - ], + }; + }, + ); - running: true - })) - }); - }); -} \ No newline at end of file + fastify.post( + "/api/v1/tunnels", + { + schema: { + body: { + type: "object", + required: ["token"], + properties: { + token: { type: "string" }, + }, + }, + }, + }, + async (req, res) => { + // @ts-ignore + const body: { + token: string; + } = req.body; + + const userData = instance.users.find(user => user.token == body.token); + + if (!userData) + return res.status(403).send({ + error: "Invalid token", + }); + + // const host = req.hostname.substring(0, req.hostname.indexOf(":")); + const unparsedPort = req.hostname.substring( + req.hostname.indexOf(":") + 1, + ); + + // @ts-ignore + // parseInt(...) can take a number just fine, at least in Node.JS + const port = parseInt(unparsedPort == "" ? proxiedPort : unparsedPort); + + // This protocol is so confusing. I'm sorry. + res.send({ + success: true, + data: instance.proxies.map(proxy => ({ + proxyUrlSettings: { + host: "sameAs", // Makes pfC work (this is by design apparently) + port, + protocol: proxy.protocol.toUpperCase(), + }, + + dest: `${proxy.sourceIP}:${proxy.destPort}`, + name: `${proxy.protocol.toUpperCase()} on ::${proxy.sourcePort} -> ::${proxy.destPort}`, + + passwords: [proxy.userConfig[userData.username]], + + running: true, + })), + }); + }, + ); +} diff --git a/api/src/backendimpl/passyfire-reimpl/socket.ts b/api/src/backendimpl/passyfire-reimpl/socket.ts index bd97620..6799765 100644 --- a/api/src/backendimpl/passyfire-reimpl/socket.ts +++ b/api/src/backendimpl/passyfire-reimpl/socket.ts @@ -9,7 +9,12 @@ 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 { +function authenticateSocket( + instance: PassyFireBackendProvider, + ws: WebSocket, + message: string, + state: ConnectedClientExt, +): Boolean { if (!message.startsWith("Accept: ")) { ws.send("400 Bad Request"); return false; @@ -25,13 +30,13 @@ function authenticateSocket(instance: PassyFireBackendProvider, ws: WebSocket, m 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"); @@ -44,7 +49,11 @@ function authenticateSocket(instance: PassyFireBackendProvider, ws: WebSocket, m return false; } -export function requestHandler(instance: PassyFireBackendProvider, ws: WebSocket, req: FastifyRequest) { +export function requestHandler( + instance: PassyFireBackendProvider, + ws: WebSocket, + req: FastifyRequest, +) { let state: "authentication" | "data" = "authentication"; let socket: dgram.Socket | net.Socket | undefined; @@ -52,25 +61,31 @@ export function requestHandler(instance: PassyFireBackendProvider, ws: WebSocket let connectedClient: ConnectedClientExt = {}; ws.on("close", () => { - instance.clients.splice(instance.clients.indexOf(connectedClient as ConnectedClientExt), 1); + 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.connect( + connectedClient.connectionDetails.sourcePort, + connectedClient.connectionDetails.sourceIP, + ); socket.on("connect", () => { state = "data"; @@ -79,7 +94,7 @@ export function requestHandler(instance: PassyFireBackendProvider, ws: WebSocket ws.send("InitProxy: Connected"); }); - socket.on("data", (data) => { + socket.on("data", data => { ws.send(data); }); } else if (connectedClient.connectionDetails.protocol == "udp") { @@ -90,7 +105,11 @@ export function requestHandler(instance: PassyFireBackendProvider, ws: WebSocket ws.send("InitProxy: Connected"); socket.on("message", (data, rinfo) => { - if (rinfo.address != connectedClient.connectionDetails.sourceIP || rinfo.port != connectedClient.connectionDetails.sourcePort) return; + if ( + rinfo.address != connectedClient.connectionDetails.sourceIP || + rinfo.port != connectedClient.connectionDetails.sourcePort + ) + return; ws.send(data); }); } @@ -98,17 +117,24 @@ export function requestHandler(instance: PassyFireBackendProvider, ws: WebSocket } 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; - }); + + 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}'`); + throw new Error( + `Whooops, our WebSocket reached an unsupported state: '${state}'`, + ); } }); -} \ No newline at end of file +} diff --git a/api/src/index.ts b/api/src/index.ts index e69b9f2..4e16bf7 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -55,7 +55,7 @@ const backends: Record = {}; const fastify = Fastify({ logger: true, - trustProxy: Boolean(process.env.IS_BEHIND_PROXY) + trustProxy: Boolean(process.env.IS_BEHIND_PROXY), }); const routeOptions: RouteOptions = {