Merge pull request #4 from greysoh/reimpl-passyfire
Reimplements PassyFire as a possible backend
This commit is contained in:
commit
5f1df9ca88
12 changed files with 618 additions and 7 deletions
83
api/package-lock.json
generated
83
api/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/websocket": "^10.0.1",
|
||||||
"@prisma/client": "^5.13.0",
|
"@prisma/client": "^5.13.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"fastify": "^4.26.2",
|
"fastify": "^4.26.2",
|
||||||
|
@ -18,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",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prisma": "^5.13.0",
|
"prisma": "^5.13.0",
|
||||||
|
@ -55,6 +57,16 @@
|
||||||
"fast-deep-equal": "^3.1.3"
|
"fast-deep-equal": "^3.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/websocket": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-8/pQIxTPRD8U94aILTeJ+2O3el/r19+Ej5z1O1mXlqplsUH7KzCjAI0sgd5DM/NoPjAi5qLFNIjgM5+9/rGSNw==",
|
||||||
|
"dependencies": {
|
||||||
|
"duplexify": "^4.1.2",
|
||||||
|
"fastify-plugin": "^4.0.0",
|
||||||
|
"ws": "^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mapbox/node-pre-gyp": {
|
"node_modules/@mapbox/node-pre-gyp": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||||
|
@ -186,6 +198,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",
|
||||||
|
@ -543,11 +564,43 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/duplexify": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1",
|
||||||
|
"stream-shift": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/duplexify/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/event-target-shim": {
|
"node_modules/event-target-shim": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
@ -663,6 +716,11 @@
|
||||||
"toad-cache": "^3.3.0"
|
"toad-cache": "^3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fastify-plugin": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ=="
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||||
|
@ -1528,6 +1586,11 @@
|
||||||
"nan": "^2.18.0"
|
"nan": "^2.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stream-shift": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
@ -1703,6 +1766,26 @@
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||||
|
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
|
|
@ -17,12 +17,14 @@
|
||||||
"@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",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prisma": "^5.13.0",
|
"prisma": "^5.13.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/websocket": "^10.0.1",
|
||||||
"@prisma/client": "^5.13.0",
|
"@prisma/client": "^5.13.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"fastify": "^4.26.2",
|
"fastify": "^4.26.2",
|
||||||
|
|
|
@ -12,12 +12,17 @@ post {
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
|
"token": "9d99397be36747b9e6f1858f1efded4756ea5b479fd5c47a6388041eecb44b4958858c6fe15f23a9cf5e9d67f48443c65342e3a69bfde231114df4bb2ab457",
|
||||||
"name": "SSH Route",
|
"name": "Passyfire Reimpl",
|
||||||
"description": "This is a test route for SSH connectivity",
|
"description": "PassyFire never dies",
|
||||||
"backend": "ssh",
|
"backend": "passyfire",
|
||||||
"connectionDetails": {
|
"connectionDetails": {
|
||||||
"funny": true
|
"ip": "127.0.0.1",
|
||||||
|
"port": 22,
|
||||||
|
|
||||||
|
"users": {
|
||||||
|
"g"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
meta {
|
||||||
|
name: Get All Scopes
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://127.0.0.1:8080/api/v1/static/getScopes
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
meta {
|
||||||
|
name: Get Tunnels
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: http://127.0.0.1:8080/api/v1/tunnels
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"token": "641d968c3bfdf78f2df86cae106349c4c95a8dd73512ee34b296379b6cd908c87b078f1f674b43c9e3394c8b233840512d88efdecf47dc63be93276f56c"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
meta {
|
||||||
|
name: Log In
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: http://127.0.0.1:8080/api/v1/users/login
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"username": "guest",
|
||||||
|
"password": "guest"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "Passyfire Base Routes",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
import type { BackendBaseClass } from "./base.js";
|
import type { BackendBaseClass } from "./base.js";
|
||||||
|
|
||||||
|
import { PassyFireBackendProvider } from "./passyfire-reimpl/index.js";
|
||||||
import { SSHBackendProvider } from "./ssh.js";
|
import { SSHBackendProvider } from "./ssh.js";
|
||||||
|
|
||||||
export const backendProviders: Record<string, typeof BackendBaseClass> = {
|
export const backendProviders: Record<string, typeof BackendBaseClass> = {
|
||||||
ssh: SSHBackendProvider,
|
"ssh": SSHBackendProvider,
|
||||||
};
|
"passyfire": PassyFireBackendProvider
|
||||||
|
};
|
185
api/src/backendimpl/passyfire-reimpl/index.ts
Normal file
185
api/src/backendimpl/passyfire-reimpl/index.ts
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
import fastifyWebsocket from "@fastify/websocket";
|
||||||
|
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import Fastify from "fastify";
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
port: number,
|
||||||
|
publicPort?: number,
|
||||||
|
isProxied?: boolean,
|
||||||
|
|
||||||
|
users: BackendProviderUser[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggedInUser = {
|
||||||
|
username: string,
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBackendProviderString(data: string): BackendParsedProviderString {
|
||||||
|
try {
|
||||||
|
JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Payload body is not JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonData = JSON.parse(data);
|
||||||
|
|
||||||
|
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 (!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");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ip: jsonData.ip,
|
||||||
|
port: jsonData.port,
|
||||||
|
|
||||||
|
publicPort: jsonData.publicPort,
|
||||||
|
isProxied: jsonData.isProxied,
|
||||||
|
|
||||||
|
users: jsonData.users
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PassyFireBackendProvider implements BackendBaseClass {
|
||||||
|
state: "stopped" | "stopping" | "started" | "starting";
|
||||||
|
|
||||||
|
clients: ConnectedClientExt[];
|
||||||
|
proxies: ForwardRuleExt[];
|
||||||
|
users: LoggedInUser[];
|
||||||
|
logs: string[];
|
||||||
|
|
||||||
|
options: BackendParsedProviderString;
|
||||||
|
fastify: FastifyInstance;
|
||||||
|
|
||||||
|
constructor(parameters: string) {
|
||||||
|
this.logs = [];
|
||||||
|
this.clients = [];
|
||||||
|
this.proxies = [];
|
||||||
|
|
||||||
|
this.state = "stopped";
|
||||||
|
this.options = parseBackendProviderString(parameters);
|
||||||
|
|
||||||
|
this.users = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
this.state = "starting";
|
||||||
|
|
||||||
|
this.fastify = Fastify({
|
||||||
|
logger: true,
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = "started";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<boolean> {
|
||||||
|
await this.fastify.close();
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const proxy: ForwardRuleExt = {
|
||||||
|
sourceIP,
|
||||||
|
sourcePort,
|
||||||
|
destPort,
|
||||||
|
protocol,
|
||||||
|
|
||||||
|
userConfig: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const user of this.options.users) {
|
||||||
|
proxy.userConfig[user.username] = generateRandomData();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.proxies.push(proxy);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 {
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static checkParametersBackendInstance(data: string): ParameterReturnedValue {
|
||||||
|
try {
|
||||||
|
parseBackendProviderString(data);
|
||||||
|
// @ts-ignore
|
||||||
|
} catch (e: Error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
163
api/src/backendimpl/passyfire-reimpl/routes.ts
Normal file
163
api/src/backendimpl/passyfire-reimpl/routes.ts
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
import { generateRandomData } from "../../libs/generateRandom.js";
|
||||||
|
import type { PassyFireBackendProvider } from "./index.js";
|
||||||
|
|
||||||
|
export function route(instance: PassyFireBackendProvider) {
|
||||||
|
const { fastify } = instance;
|
||||||
|
|
||||||
|
const proxiedPort: number = instance.options.publicPort ?? 443;
|
||||||
|
|
||||||
|
const unsupportedSpoofedRoutes: string[] = [
|
||||||
|
"/api/v1/tunnels/add",
|
||||||
|
"/api/v1/tunnels/edit",
|
||||||
|
"/api/v1/tunnels/remove",
|
||||||
|
|
||||||
|
// TODO (greysoh): Should we implement these? We have these for internal reasons. We could expose these /shrug
|
||||||
|
"/api/v1/tunnels/start",
|
||||||
|
"/api/v1/tunnels/stop",
|
||||||
|
|
||||||
|
// Same scenario for this API.
|
||||||
|
"/api/v1/users",
|
||||||
|
"/api/v1/users/add",
|
||||||
|
"/api/v1/users/remove",
|
||||||
|
"/api/v1/users/enable",
|
||||||
|
"/api/v1/users/disable",
|
||||||
|
];
|
||||||
|
|
||||||
|
fastify.get("/api/v1/static/getScopes", () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
users: {
|
||||||
|
add: true,
|
||||||
|
remove: true,
|
||||||
|
get: true,
|
||||||
|
getPasswords: true
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
add: true,
|
||||||
|
remove: true,
|
||||||
|
start: true,
|
||||||
|
stop: true,
|
||||||
|
get: 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"
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(req.body);
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(400).send({
|
||||||
|
error: "Invalid token"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (!req.body.token) return res.status(400).send({
|
||||||
|
error: "Invalid token"
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(403).send({
|
||||||
|
error: "Invalid scope(s)"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, 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
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
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}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -55,6 +55,7 @@ const backends: Record<number, BackendBaseClass> = {};
|
||||||
|
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
|
trustProxy: Boolean(process.env.IS_BEHIND_PROXY)
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeOptions: RouteOptions = {
|
const routeOptions: RouteOptions = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue