chore: Restructure files.

This commit is contained in:
imterah 2024-12-21 18:27:40 -05:00
parent 559588f726
commit d25da9091e
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
93 changed files with 38 additions and 26 deletions

17
backend-legacy/Dockerfile Normal file
View file

@ -0,0 +1,17 @@
FROM node:22.11.0-bookworm
LABEL org.opencontainers.image.source="https://github.com/greysoh/nextnet"
WORKDIR /app/
COPY src /app/src
COPY prisma /app/prisma
COPY docker-entrypoint.sh /app/
COPY tsconfig.json /app/
COPY package.json /app/
COPY package-lock.json /app/
COPY srcpatch.sh /app/
RUN sh srcpatch.sh
RUN npm install --save-dev
RUN npm run build
RUN rm srcpatch.sh out/**/*.ts out/**/*.map
RUN rm -rf src
RUN npm prune --production
ENTRYPOINT sh docker-entrypoint.sh

7
backend-legacy/dev.env Normal file
View file

@ -0,0 +1,7 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://nextnet:nextnet@localhost:5432/nextnet?schema=nextnet"

View file

@ -0,0 +1,12 @@
#!/bin/bash
export NODE_ENV="production"
if [[ "$DATABASE_URL" == "" ]]; then
export DATABASE_URL="postgresql://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@nextnet-postgres:5432/$POSTGRES_DB?schema=nextnet"
fi
echo "Welcome to NextNet."
echo "Running database migrations..."
npx prisma migrate deploy
echo "Starting application..."
npm start

View file

@ -0,0 +1,19 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
globals: globals.node,
},
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
];

3302
backend-legacy/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
{
"name": "nextnet",
"version": "1.1.2",
"description": "Yet another dashboard to manage portforwarding technologies",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "cd out && node --enable-source-maps index.js",
"dev": "nodemon --watch src --ext ts,js,mjs,json --exec \"tsc && cd out && node --enable-source-maps index.js\""
},
"keywords": [],
"author": "greysoh",
"license": "BSD-3-Clause",
"devDependencies": {
"@eslint/js": "^9.16.0",
"@types/bcrypt": "^5.0.2",
"@types/node": "^22.10.1",
"@types/ssh2": "^1.15.1",
"@types/ws": "^8.5.13",
"eslint": "^9.16.0",
"globals": "^15.12.0",
"nodemon": "^3.1.7",
"pino-pretty": "^13.0.0",
"prettier": "^3.4.1",
"prisma": "^5.22.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.16.0"
},
"dependencies": {
"@fastify/websocket": "^11.0.1",
"@prisma/client": "^6.0.0",
"bcrypt": "^5.1.1",
"fastify": "^5.1.0",
"node-ssh": "^13.2.0"
}
}

View file

@ -0,0 +1,53 @@
-- CreateTable
CREATE TABLE "DesinationProvider" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backend" TEXT NOT NULL,
"connectionDetails" TEXT NOT NULL,
CONSTRAINT "DesinationProvider_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ForwardRule" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"sourceIP" TEXT NOT NULL,
"sourcePort" INTEGER NOT NULL,
"destIP" TEXT NOT NULL,
"destPort" INTEGER NOT NULL,
"destProviderID" INTEGER NOT NULL,
"enabled" BOOLEAN NOT NULL,
CONSTRAINT "ForwardRule_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Permission" (
"id" SERIAL NOT NULL,
"permission" TEXT NOT NULL,
"has" BOOLEAN NOT NULL,
"userID" INTEGER NOT NULL,
CONSTRAINT "Permission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL,
"rootToken" TEXT,
"isRootServiceAccount" BOOLEAN,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `destIP` on the `ForwardRule` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "ForwardRule" DROP COLUMN "destIP";

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `protocol` to the `ForwardRule` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "ForwardRule" ADD COLUMN "protocol" TEXT NOT NULL;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "username" TEXT;

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View file

@ -0,0 +1,54 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model DesinationProvider {
id Int @id @default(autoincrement())
name String
description String?
backend String
connectionDetails String
}
model ForwardRule {
id Int @id @default(autoincrement())
name String
description String?
protocol String
sourceIP String
sourcePort Int
destPort Int
destProviderID Int
enabled Boolean
}
model Permission {
id Int @id @default(autoincrement())
permission String
has Boolean
user User @relation(fields: [userID], references: [id])
userID Int
}
model User {
id Int @id @default(autoincrement())
email String @unique
username String? // NOT optional in the API, but just for backwards compat
name String
password String // Will be hashed using bcrypt
rootToken String?
isRootServiceAccount Boolean?
permissions Permission[]
}

View file

@ -0,0 +1,28 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/backends/create
body: json
auth: none
}
body:json {
{
"token": "9d99397be36747b9e6f1858f1efded4756ea5b479fd5c47a6388041eecb44b4958858c6fe15f23a9cf5e9d67f48443c65342e3a69bfde231114df4bb2ab457",
"name": "Passyfire Reimpl",
"description": "PassyFire never dies",
"backend": "passyfire",
"connectionDetails": {
"ip": "127.0.0.1",
"port": 22,
"users": {
"g"
}
}
}
}

View file

@ -0,0 +1,17 @@
meta {
name: Lookup
type: http
seq: 3
}
post {
url: http://127.0.0.1:3000/api/v1/backends/lookup
body: json
auth: none
}
body:json {
{
"token": "7d69814cdada551dd22521ad97b23b22a106278826a2b4e87dd76246594b56f973894e8265437a5d520ed7258d7c856d0d294e89b1de1a98db7fa4a"
}
}

View file

@ -0,0 +1,23 @@
meta {
name: Remove
type: http
seq: 2
}
post {
url: http://127.0.0.1:3000/api/v1/backends/create
body: json
auth: none
}
body:json {
{
"token": "f1b89cc337073476289ade17ffbe7a6419b4bd52aa7ede26114bffd76fa263b5cb1bcaf389462e1d9e7acb7f4b6a7c28152a9cc9af83e3ec862f1892b1",
"name": "PortCopier Route",
"description": "This is a test route for portcopier.",
"backend": "PortCopier",
"connectionDetails": {
"funny": true
}
}
}

View file

@ -0,0 +1,28 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/forward/create
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"name": "Test Route",
"description": "This is a test route for SSH",
"protocol": "tcp",
"sourceIP": "127.0.0.1",
"sourcePort": "8000",
"destinationPort": "9000",
"providerID": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Get Inbound Connections
type: http
seq: 6
}
post {
url: http://127.0.0.1:3000/api/v1/forward/connections
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"id": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Lookup
type: http
seq: 3
}
post {
url: http://127.0.0.1:3000/api/v1/forward/lookup
body: json
auth: none
}
body:json {
{
"token": "535c80825631c04b9add7a8682e06799d62ba57b5089b557f5bab2183fc9926b187b3b8d96da8ef16c67ec80f2917cf81bc21337f47728534f58ac9c4ed5f3fe",
"protocol": "tcp"
}
}

View file

@ -0,0 +1,26 @@
meta {
name: Remove
type: http
seq: 2
}
post {
url: http://127.0.0.1:3000/api/v1/forward/remove
body: json
auth: none
}
body:json {
{
"token": "f1b89cc337073476289ade17ffbe7a6419b4bd52aa7ede26114bffd76fa263b5cb1bcaf389462e1d9e7acb7f4b6a7c28152a9cc9af83e3ec862f1892b1",
"name": "Test Route",
"description": "This is a test route for portcopier.",
"sourceIP": "127.0.0.1",
"sourcePort": "8000",
"destinationPort": "9000",
"providerID": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Start
type: http
seq: 4
}
post {
url: http://127.0.0.1:3000/api/v1/forward/start
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"id": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Stop
type: http
seq: 5
}
post {
url: http://127.0.0.1:3000/api/v1/forward/stop
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"id": "1"
}
}

View file

@ -0,0 +1,17 @@
meta {
name: Get Permissions
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/getPermissions
body: json
auth: none
}
body:json {
{
"token": "5e2cb92a338a832d385790861312eb85d69f46f82317bfa984ac5e3517368ab5a827897b0f9775a9181b02fa3b9cffed7e59e5b3111d5bdc37f729156caf5f"
}
}

View file

@ -0,0 +1,19 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/users/create
body: json
auth: inherit
}
body:json {
{
"name": "Greysoh Hofuh",
"email": "greyson@hofers.cloud",
"password": "hunter123"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Log In
type: http
seq: 2
}
post {
url: http://127.0.0.1:3000/api/v1/users/login
body: json
auth: none
}
body:json {
{
"email": "me@greysoh.dev",
"password": "password"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Lookup
type: http
seq: 4
}
post {
url: http://127.0.0.1:3000/api/v1/users/lookup
body: json
auth: none
}
body:json {
{
"token": "5e2cb92a338a832d385790861312eb85d69f46f82317bfa984ac5e3517368ab5a827897b0f9775a9181b02fa3b9cffed7e59e5b3111d5bdc37f729156caf5f",
"name": "Greyson Hofer"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Remove
type: http
seq: 3
}
post {
url: http://127.0.0.1:3000/api/v1/users/remove
body: json
auth: inherit
}
body:json {
{
"token": "5e2cb92a338a832d385790861312eb85d69f46f82317bfa984ac5e3517368ab5a827897b0f9775a9181b02fa3b9cffed7e59e5b3111d5bdc37f729156caf5f",
"uid": "2"
}
}

View file

@ -0,0 +1,5 @@
{
"version": "1",
"name": "NextNet API",
"type": "collection"
}

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
npx @usebruno/cli run "$1" --output /tmp/out.json
cat /tmp/out.json | less

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,9 @@
{
"version": "1",
"name": "Passyfire Base Routes",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View file

@ -0,0 +1,77 @@
// @eslint-ignore-file
export type ParameterReturnedValue = {
success: boolean;
message?: string;
};
export type ForwardRule = {
sourceIP: string;
sourcePort: number;
destPort: number;
};
export type ConnectedClient = {
ip: string;
port: number;
connectionDetails: ForwardRule;
};
export class BackendBaseClass {
state: "stopped" | "stopping" | "started" | "starting";
clients?: ConnectedClient[]; // Not required to be implemented, but more consistency
logs: string[];
constructor(parameters: string) {
this.logs = [];
this.clients = [];
this.state = "stopped";
}
addConnection(
sourceIP: string,
sourcePort: number,
destPort: number,
protocol: "tcp" | "udp",
): void {}
removeConnection(
sourceIP: string,
sourcePort: number,
destPort: number,
protocol: "tcp" | "udp",
): void {}
async start(): Promise<boolean> {
return true;
}
async stop(): Promise<boolean> {
return true;
}
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 {
return {
success: true,
};
}
}

View file

@ -0,0 +1,13 @@
import { BackendBaseClass } from "./base.js";
import { PassyFireBackendProvider } from "./passyfire-reimpl/index.js";
import { SSHBackendProvider } from "./ssh.js";
export const backendProviders: Record<string, typeof BackendBaseClass> = {
ssh: SSHBackendProvider,
passyfire: PassyFireBackendProvider,
};
if (process.env.NODE_ENV != "production") {
backendProviders["dummy"] = BackendBaseClass;
}

View file

@ -0,0 +1,231 @@
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-expect-error: We write the function, and we know we're returning an error
} catch (e: Error) {
return {
success: false,
message: e.toString(),
};
}
return {
success: true,
};
}
}

View file

@ -0,0 +1,158 @@
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) => {
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-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
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-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
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-expect-error: 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,
})),
});
},
);
}

View file

@ -0,0 +1,140 @@
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-expect-error: FIXME because this is a mess
const 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}'`,
);
}
});
}

View file

@ -0,0 +1,331 @@
import { NodeSSH } from "node-ssh";
import { Socket } from "node:net";
import type {
BackendBaseClass,
ForwardRule,
ConnectedClient,
ParameterReturnedValue,
} from "./base.js";
import {
TcpConnectionDetails,
AcceptConnection,
ClientChannel,
RejectConnection,
} from "ssh2";
type ForwardRuleExt = ForwardRule & {
enabled: boolean;
};
// Fight me (for better naming)
type BackendParsedProviderString = {
ip: string;
port: number;
username: string;
privateKey: string;
listenOnIPs: 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.username != "string") {
throw new Error("Username is not a string");
}
if (typeof jsonData.privateKey != "string") {
throw new Error("Private key is not a string");
}
let listenOnIPs: string[] = [];
if (!Array.isArray(jsonData.listenOnIPs)) {
listenOnIPs.push("0.0.0.0");
} else {
listenOnIPs = jsonData.listenOnIPs;
}
return {
ip: jsonData.ip,
port: jsonData.port,
username: jsonData.username,
privateKey: jsonData.privateKey,
listenOnIPs,
};
}
export class SSHBackendProvider implements BackendBaseClass {
state: "stopped" | "stopping" | "started" | "starting";
clients: ConnectedClient[];
proxies: ForwardRuleExt[];
logs: string[];
sshInstance: NodeSSH;
options: BackendParsedProviderString;
constructor(parameters: string) {
this.logs = [];
this.proxies = [];
this.clients = [];
this.options = parseBackendProviderString(parameters);
this.state = "stopped";
}
async start(): Promise<boolean> {
this.state = "starting";
this.logs.push("Starting SSHBackendProvider...");
if (this.sshInstance) {
this.sshInstance.dispose();
}
this.sshInstance = new NodeSSH();
try {
await this.sshInstance.connect({
host: this.options.ip,
port: this.options.port,
username: this.options.username,
privateKey: this.options.privateKey,
});
} catch (e) {
this.logs.push(`Failed to start SSHBackendProvider! Error: '${e}'`);
this.state = "stopped";
// @ts-expect-error: We know that stuff will be initialized in order, so this will be safe
this.sshInstance = null;
return false;
}
if (this.sshInstance.connection) {
this.sshInstance.connection.on("end", async () => {
if (this.state != "started") return;
this.logs.push("We disconnected from the SSH server. Restarting...");
// Create a new array from the existing list of proxies, so we have a backup of the proxy list before
// we wipe the list of all proxies and clients (as we're disconnected anyways)
const proxies = Array.from(this.proxies);
this.proxies.splice(0, this.proxies.length);
this.clients.splice(0, this.clients.length);
await this.start();
if (this.state != "started") return;
for (const proxy of proxies) {
if (!proxy.enabled) continue;
this.addConnection(
proxy.sourceIP,
proxy.sourcePort,
proxy.destPort,
"tcp",
);
}
});
}
this.state = "started";
this.logs.push("Successfully started SSHBackendProvider.");
return true;
}
async stop(): Promise<boolean> {
this.state = "stopping";
this.logs.push("Stopping SSHBackendProvider...");
this.proxies.splice(0, this.proxies.length);
this.sshInstance.dispose();
// @ts-expect-error: We know that stuff will be initialized in order, so this will be safe
this.sshInstance = null;
this.logs.push("Successfully stopped SSHBackendProvider.");
this.state = "stopped";
return true;
}
addConnection(
sourceIP: string,
sourcePort: number,
destPort: number,
protocol: "tcp" | "udp",
): void {
const connectionCheck = SSHBackendProvider.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;
const connCallback = (
info: TcpConnectionDetails,
accept: AcceptConnection<ClientChannel>,
reject: RejectConnection,
) => {
const foundProxyEntry = this.proxies.find(
i =>
i.sourceIP == sourceIP &&
i.sourcePort == sourcePort &&
i.destPort == destPort,
);
if (!foundProxyEntry || !foundProxyEntry.enabled) return reject();
const client: ConnectedClient = {
ip: info.srcIP,
port: info.srcPort,
connectionDetails: foundProxyEntry,
};
this.clients.push(client);
const srcConn = new Socket();
srcConn.connect({
host: sourceIP,
port: sourcePort,
});
// Why is this so confusing
const destConn = accept();
destConn.addListener("data", (chunk: Uint8Array) => {
srcConn.write(chunk);
});
destConn.addListener("end", () => {
this.clients.splice(this.clients.indexOf(client), 1);
srcConn.end();
});
srcConn.on("data", data => {
destConn.write(data);
});
srcConn.on("end", () => {
this.clients.splice(this.clients.indexOf(client), 1);
destConn.end();
});
};
for (const ip of this.options.listenOnIPs) {
this.sshInstance.forwardIn(ip, destPort, connCallback);
}
this.proxies.push({
sourceIP,
sourcePort,
destPort,
enabled: true,
});
}
removeConnection(
sourceIP: string,
sourcePort: number,
destPort: number,
protocol: "tcp" | "udp",
): void {
const connectionCheck = SSHBackendProvider.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;
foundProxyEntry.enabled = false;
}
getAllConnections(): ConnectedClient[] {
return this.clients;
}
static checkParametersConnection(
sourceIP: string,
sourcePort: number,
destPort: number,
protocol: "tcp" | "udp",
): ParameterReturnedValue {
if (protocol == "udp") {
return {
success: false,
message:
"SSH does not support UDP tunneling! Please use something like PortCopier instead (if it gets done)",
};
}
return {
success: true,
};
}
static checkParametersBackendInstance(data: string): ParameterReturnedValue {
try {
parseBackendProviderString(data);
// @ts-expect-error: We write the function, and we know we're returning an error
} catch (e: Error) {
return {
success: false,
message: e.toString(),
};
}
return {
success: true,
};
}
}

140
backend-legacy/src/index.ts Normal file
View file

@ -0,0 +1,140 @@
import process from "node:process";
import { PrismaClient } from "@prisma/client";
import Fastify from "fastify";
import type {
ServerOptions,
SessionToken,
RouteOptions,
} from "./libs/types.js";
import type { BackendBaseClass } from "./backendimpl/base.js";
import { route as getPermissions } from "./routes/getPermissions.js";
import { route as backendCreate } from "./routes/backends/create.js";
import { route as backendRemove } from "./routes/backends/remove.js";
import { route as backendLookup } from "./routes/backends/lookup.js";
import { route as forwardConnections } from "./routes/forward/connections.js";
import { route as forwardCreate } from "./routes/forward/create.js";
import { route as forwardRemove } from "./routes/forward/remove.js";
import { route as forwardLookup } from "./routes/forward/lookup.js";
import { route as forwardStart } from "./routes/forward/start.js";
import { route as forwardStop } from "./routes/forward/stop.js";
import { route as userCreate } from "./routes/user/create.js";
import { route as userRemove } from "./routes/user/remove.js";
import { route as userLookup } from "./routes/user/lookup.js";
import { route as userLogin } from "./routes/user/login.js";
import { backendInit } from "./libs/backendInit.js";
const prisma = new PrismaClient();
const isSignupEnabled = Boolean(process.env.IS_SIGNUP_ENABLED);
const unsafeAdminSignup = Boolean(process.env.UNSAFE_ADMIN_SIGNUP);
const noUsersCheck = (await prisma.user.count()) == 0;
if (unsafeAdminSignup) {
console.error(
"WARNING: You have admin sign up on! This means that anyone that signs up will have admin rights!",
);
}
const serverOptions: ServerOptions = {
isSignupEnabled: isSignupEnabled ? true : noUsersCheck,
isSignupAsAdminEnabled: unsafeAdminSignup ? true : noUsersCheck,
allowUnsafeGlobalTokens: process.env.NODE_ENV != "production",
};
const sessionTokens: Record<number, SessionToken[]> = {};
const backends: Record<number, BackendBaseClass> = {};
const loggerEnv = {
development: {
transport: {
target: "pino-pretty",
options: {
translateTime: "HH:MM:ss Z",
ignore: "pid,hostname,time",
},
},
},
production: true,
test: false,
};
const fastify = Fastify({
logger:
process.env.NODE_ENV == "production"
? loggerEnv.production
: loggerEnv.development,
trustProxy: Boolean(process.env.IS_BEHIND_PROXY),
});
const routeOptions: RouteOptions = {
fastify: fastify,
prisma: prisma,
tokens: sessionTokens,
options: serverOptions,
backends: backends,
};
fastify.log.info("Initializing forwarding rules...");
const createdBackends = await prisma.desinationProvider.findMany();
const logWrapper = (arg: string) => fastify.log.info(arg);
const errorWrapper = (arg: string) => fastify.log.error(arg);
for (const backend of createdBackends) {
fastify.log.info(
`Running init steps for ID '${backend.id}' (${backend.name})`,
);
const init = await backendInit(
backend,
backends,
prisma,
logWrapper,
errorWrapper,
);
if (init) fastify.log.info("Init successful.");
}
fastify.log.info("Done.");
getPermissions(routeOptions);
backendCreate(routeOptions);
backendRemove(routeOptions);
backendLookup(routeOptions);
forwardConnections(routeOptions);
forwardCreate(routeOptions);
forwardRemove(routeOptions);
forwardLookup(routeOptions);
forwardStart(routeOptions);
forwardStop(routeOptions);
userCreate(routeOptions);
userRemove(routeOptions);
userLookup(routeOptions);
userLogin(routeOptions);
// Run the server!
try {
await fastify.listen({
port: 3000,
host: process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1",
});
} catch (err) {
fastify.log.error(err);
process.exit(1);
}

View file

@ -0,0 +1,84 @@
import { format } from "node:util";
import type { PrismaClient } from "@prisma/client";
import { backendProviders } from "../backendimpl/index.js";
import { BackendBaseClass } from "../backendimpl/base.js";
type Backend = {
id: number;
name: string;
description: string | null;
backend: string;
connectionDetails: string;
};
export async function backendInit(
backend: Backend,
backends: Record<number, BackendBaseClass>,
prisma: PrismaClient,
logger?: (arg: string) => void,
errorOut?: (arg: string) => void,
): Promise<boolean> {
const log = (...args: string[]) =>
logger ? logger(format(...args)) : console.log(...args);
const error = (...args: string[]) =>
errorOut ? errorOut(format(...args)) : log(...args);
const ourProvider = backendProviders[backend.backend];
if (!ourProvider) {
error(" - Error: Invalid backend recieved!");
// Prevent crashes when we don't recieve a backend
backends[backend.id] = new BackendBaseClass("");
backends[backend.id].logs.push("** Failed To Create Backend **");
backends[backend.id].logs.push(
"Reason: Invalid backend recieved (couldn't find the backend to use!)",
);
return false;
}
log(" - Initializing backend...");
backends[backend.id] = new ourProvider(backend.connectionDetails);
const ourBackend = backends[backend.id];
if (!(await ourBackend.start())) {
error(" - Error initializing backend!");
error(" - " + ourBackend.logs.join("\n - "));
return false;
}
log(" - Initializing clients...");
const clients = await prisma.forwardRule.findMany({
where: {
destProviderID: backend.id,
enabled: true,
},
});
for (const client of clients) {
if (client.protocol != "tcp" && client.protocol != "udp") {
error(
` - Error: Client with ID of '${client.id}' has an invalid protocol! (must be either TCP or UDP)`,
);
continue;
}
ourBackend.addConnection(
client.sourceIP,
client.sourcePort,
client.destPort,
client.protocol,
);
}
return true;
}

View file

@ -0,0 +1,22 @@
function getRandomInt(min: number, max: number): number {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}
export function generateRandomData(length: number = 128): string {
let newString = "";
for (let i = 0; i < length; i += 2) {
const randomNumber = getRandomInt(0, 255);
if (randomNumber == 0) {
i -= 2;
continue;
}
newString += randomNumber.toString(16);
}
return newString;
}

View file

@ -0,0 +1,110 @@
import type { PrismaClient } from "@prisma/client";
import type { SessionToken } from "./types.js";
export const permissionListDisabled: Record<string, boolean> = {
"routes.add": false,
"routes.remove": false,
"routes.start": false,
"routes.stop": false,
"routes.edit": false,
"routes.visible": false,
"routes.visibleConn": false,
"backends.add": false,
"backends.remove": false,
"backends.start": false,
"backends.stop": false,
"backends.edit": false,
"backends.visible": false,
"backends.secretVis": false,
"permissions.see": false,
"users.add": false,
"users.remove": false,
"users.lookup": false,
"users.edit": false,
};
// FIXME: This solution fucking sucks.
export const permissionListEnabled: Record<string, boolean> = JSON.parse(
JSON.stringify(permissionListDisabled),
);
for (const index of Object.keys(permissionListEnabled)) {
permissionListEnabled[index] = true;
}
export async function hasPermission(
permissionList: string[],
uid: number,
prisma: PrismaClient,
): Promise<boolean> {
for (const permission of permissionList) {
const permissionNode = await prisma.permission.findFirst({
where: {
userID: uid,
permission,
},
});
if (!permissionNode || !permissionNode.has) return false;
}
return true;
}
export async function getUID(
token: string,
tokens: Record<number, SessionToken[]>,
prisma: PrismaClient,
): Promise<number> {
let userID = -1;
// Look up in our currently authenticated users
for (const otherTokenKey of Object.keys(tokens)) {
const otherTokenList = tokens[parseInt(otherTokenKey)];
for (const otherTokenIndex in otherTokenList) {
const otherToken = otherTokenList[otherTokenIndex];
if (otherToken.token == token) {
if (
otherToken.expiresAt <
otherToken.createdAt + (otherToken.createdAt - Date.now())
) {
otherTokenList.splice(parseInt(otherTokenIndex), 1);
continue;
} else {
userID = parseInt(otherTokenKey);
}
}
}
}
// Fine, we'll look up for global tokens...
// FIXME: Could this be more efficient? IDs are sequential in SQL I think
if (userID == -1) {
const allUsers = await prisma.user.findMany({
where: {
isRootServiceAccount: true,
},
});
for (const user of allUsers) {
if (user.rootToken == token) userID = user.id;
}
}
return userID;
}
export async function hasPermissionByToken(
permissionList: string[],
token: string,
tokens: Record<number, SessionToken[]>,
prisma: PrismaClient,
): Promise<boolean> {
const userID = await getUID(token, tokens, prisma);
return await hasPermission(permissionList, userID, prisma);
}

View file

@ -0,0 +1,28 @@
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import type { BackendBaseClass } from "../backendimpl/base.js";
export type ServerOptions = {
isSignupEnabled: boolean;
isSignupAsAdminEnabled: boolean;
allowUnsafeGlobalTokens: boolean;
};
// NOTE: Someone should probably use Redis for this, but this is fine...
export type SessionToken = {
createdAt: number;
expiresAt: number; // Should be (createdAt + (30 minutes))
token: string;
};
export type RouteOptions = {
fastify: FastifyInstance;
prisma: PrismaClient;
tokens: Record<number, SessionToken[]>;
options: ServerOptions;
backends: Record<number, BackendBaseClass>;
};

View file

@ -0,0 +1,17 @@
# Route Plan
- [x] /api/v1/users/create
- [x] /api/v1/users/login
- [x] /api/v1/users/remove
- [ ] /api/v1/users/modify
- [x] /api/v1/users/lookup
- [x] /api/v1/backends/create
- [x] /api/v1/backends/remove
- [ ] /api/v1/backends/modify
- [x] /api/v1/backends/lookup
- [x] /api/v1/routes/create
- [x] /api/v1/routes/remove
- [ ] /api/v1/routes/modify
- [x] /api/v1/routes/lookup
- [ ] /api/v1/routes/start
- [ ] /api/v1/routes/stop
- [x] /api/v1/getPermissions

View file

@ -0,0 +1,107 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
import { backendProviders } from "../../backendimpl/index.js";
import { backendInit } from "../../libs/backendInit.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens, backends } = routeOptions;
const logWrapper = (arg: string) => fastify.log.info(arg);
const errorWrapper = (arg: string) => fastify.log.error(arg);
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new backend to use
*/
fastify.post(
"/api/v1/backends/create",
{
schema: {
body: {
type: "object",
required: ["token", "name", "backend", "connectionDetails"],
properties: {
token: { type: "string" },
name: { type: "string" },
description: { type: "string" },
backend: { type: "string" },
connectionDetails: { type: "string" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
name: string;
description?: string;
connectionDetails: string;
backend: string;
} = req.body;
if (!(await hasPermission(body.token, ["backends.add"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
if (!backendProviders[body.backend]) {
return res.status(400).send({
error: "Unsupported backend!",
});
}
const connectionDetailsValidityCheck = backendProviders[
body.backend
].checkParametersBackendInstance(body.connectionDetails);
if (!connectionDetailsValidityCheck.success) {
return res.status(400).send({
error:
connectionDetailsValidityCheck.message ??
"Unknown error while attempting to parse connectionDetails (it's on your side)",
});
}
const backend = await prisma.desinationProvider.create({
data: {
name: body.name,
description: body.description,
backend: body.backend,
connectionDetails: body.connectionDetails,
},
});
const init = await backendInit(
backend,
backends,
prisma,
logWrapper,
errorWrapper,
);
if (!init) {
// TODO: better error code
return res.status(504).send({
error: "Backend is created, but failed to initalize correctly",
id: backend.id,
});
}
return {
success: true,
id: backend.id,
};
},
);
}

View file

@ -0,0 +1,84 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens, backends } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new route to use
*/
fastify.post(
"/api/v1/backends/lookup",
{
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
id: { type: "number" },
name: { type: "string" },
description: { type: "string" },
backend: { type: "string" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id?: number;
name?: string;
description?: string;
backend?: string;
} = req.body;
if (
!(await hasPermission(body.token, [
"backends.visible", // wtf?
]))
) {
return res.status(403).send({
error: "Unauthorized",
});
}
const canSeeSecrets = await hasPermission(body.token, [
"backends.secretVis",
]);
const prismaBackends = await prisma.desinationProvider.findMany({
where: {
id: body.id,
name: body.name,
description: body.description,
backend: body.backend,
},
});
return {
success: true,
data: prismaBackends.map(i => ({
id: i.id,
name: i.name,
description: i.description,
backend: i.backend,
connectionDetails: canSeeSecrets ? i.connectionDetails : "",
logs: backends[i.id].logs,
})),
};
},
);
}

View file

@ -0,0 +1,71 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens, backends } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new route to use
*/
fastify.post(
"/api/v1/backends/remove",
{
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id: number;
} = req.body;
if (!(await hasPermission(body.token, ["backends.remove"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
if (!backends[body.id]) {
return res.status(400).send({
error: "Backend not found",
});
}
// Unload the backend
if (!(await backends[body.id].stop())) {
return res.status(400).send({
error: "Failed to stop backend! Please report this issue.",
});
}
delete backends[body.id];
await prisma.desinationProvider.delete({
where: {
id: body.id,
},
});
return {
success: true,
};
},
);
}

View file

@ -0,0 +1,72 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens, backends } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
fastify.post(
"/api/v1/forward/connections",
{
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id: number;
} = req.body;
if (!(await hasPermission(body.token, ["routes.visibleConn"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
const forward = await prisma.forwardRule.findUnique({
where: {
id: body.id,
},
});
if (!forward) {
return res.status(400).send({
error: "Could not find forward entry",
});
}
if (!backends[forward.destProviderID]) {
return res.status(400).send({
error: "Backend not found",
});
}
return {
success: true,
data: backends[forward.destProviderID].getAllConnections().filter(i => {
return (
i.connectionDetails.sourceIP == forward.sourceIP &&
i.connectionDetails.sourcePort == forward.sourcePort &&
i.connectionDetails.destPort == forward.destPort
);
}),
};
},
);
}

View file

@ -0,0 +1,119 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new route to use
*/
fastify.post(
"/api/v1/forward/create",
{
schema: {
body: {
type: "object",
required: [
"token",
"name",
"protocol",
"sourceIP",
"sourcePort",
"destinationPort",
"providerID",
],
properties: {
token: { type: "string" },
name: { type: "string" },
description: { type: "string" },
protocol: { type: "string" },
sourceIP: { type: "string" },
sourcePort: { type: "number" },
destinationPort: { type: "number" },
providerID: { type: "number" },
autoStart: { type: "boolean" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
name: string;
description?: string;
protocol: "tcp" | "udp";
sourceIP: string;
sourcePort: number;
destinationPort: number;
providerID: number;
autoStart?: boolean;
} = req.body;
if (body.protocol != "tcp" && body.protocol != "udp") {
return res.status(400).send({
error: "Body protocol field must be either tcp or udp",
});
}
if (!(await hasPermission(body.token, ["routes.add"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
const lookupIDForDestProvider =
await prisma.desinationProvider.findUnique({
where: {
id: body.providerID,
},
});
if (!lookupIDForDestProvider)
return res.status(400).send({
error: "Could not find provider",
});
const forwardRule = await prisma.forwardRule.create({
data: {
name: body.name,
description: body.description,
protocol: body.protocol,
sourceIP: body.sourceIP,
sourcePort: body.sourcePort,
destPort: body.destinationPort,
destProviderID: body.providerID,
enabled: Boolean(body.autoStart),
},
});
return {
success: true,
id: forwardRule.id,
};
},
);
}

View file

@ -0,0 +1,113 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new route to use
*/
fastify.post(
"/api/v1/forward/lookup",
{
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
id: { type: "number" },
name: { type: "string" },
protocol: { type: "string" },
description: { type: "string" },
sourceIP: { type: "string" },
sourcePort: { type: "number" },
destPort: { type: "number" },
providerID: { type: "number" },
autoStart: { type: "boolean" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id?: number;
name?: string;
description?: string;
protocol?: "tcp" | "udp";
sourceIP?: string;
sourcePort?: number;
destinationPort?: number;
providerID?: number;
autoStart?: boolean;
} = req.body;
if (body.protocol && body.protocol != "tcp" && body.protocol != "udp") {
return res.status(400).send({
error: "Protocol specified in body must be either 'tcp' or 'udp'",
});
}
if (
!(await hasPermission(body.token, [
"routes.visible", // wtf?
]))
) {
return res.status(403).send({
error: "Unauthorized",
});
}
const forwardRules = await prisma.forwardRule.findMany({
where: {
id: body.id,
name: body.name,
description: body.description,
sourceIP: body.sourceIP,
sourcePort: body.sourcePort,
destPort: body.destinationPort,
destProviderID: body.providerID,
enabled: body.autoStart,
},
});
return {
success: true,
data: forwardRules.map(i => ({
id: i.id,
name: i.name,
description: i.description,
sourceIP: i.sourceIP,
sourcePort: i.sourcePort,
destPort: i.destPort,
providerID: i.destProviderID,
autoStart: i.enabled, // TODO: Add enabled flag in here to see if we're running or not
})),
};
},
);
}

View file

@ -0,0 +1,56 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new route to use
*/
fastify.post(
"/api/v1/forward/remove",
{
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id: number;
} = req.body;
if (!(await hasPermission(body.token, ["routes.remove"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
await prisma.forwardRule.delete({
where: {
id: body.id,
},
});
return {
success: true,
};
},
);
}

View file

@ -0,0 +1,76 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens, backends } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new route to use
*/
fastify.post(
"/api/v1/forward/start",
{
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id: number;
} = req.body;
if (!(await hasPermission(body.token, ["routes.start"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
const forward = await prisma.forwardRule.findUnique({
where: {
id: body.id,
},
});
if (!forward)
return res.status(400).send({
error: "Could not find forward entry",
});
if (!backends[forward.destProviderID])
return res.status(400).send({
error: "Backend not found",
});
// @ts-expect-error: Other restrictions in place make it so that it MUST be either TCP or UDP
const protocol: "tcp" | "udp" = forward.protocol;
backends[forward.destProviderID].addConnection(
forward.sourceIP,
forward.sourcePort,
forward.destPort,
protocol,
);
return {
success: true,
};
},
);
}

View file

@ -0,0 +1,76 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens, backends } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new route to use
*/
fastify.post(
"/api/v1/forward/stop",
{
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id: number;
} = req.body;
if (!(await hasPermission(body.token, ["routes.stop"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
const forward = await prisma.forwardRule.findUnique({
where: {
id: body.id,
},
});
if (!forward)
return res.status(400).send({
error: "Could not find forward entry",
});
if (!backends[forward.destProviderID])
return res.status(400).send({
error: "Backend not found",
});
// @ts-expect-error: Other restrictions in place make it so that it MUST be either TCP or UDP
const protocol: "tcp" | "udp" = forward.protocol;
backends[forward.destProviderID].removeConnection(
forward.sourceIP,
forward.sourcePort,
forward.destPort,
protocol,
);
return {
success: true,
};
},
);
}

View file

@ -0,0 +1,51 @@
import { hasPermission, getUID } from "../libs/permissions.js";
import type { RouteOptions } from "../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens } = routeOptions;
/**
* Logs in to a user account.
*/
fastify.post(
"/api/v1/getPermissions",
{
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
} = req.body;
const uid = await getUID(body.token, tokens, prisma);
if (!(await hasPermission(["permissions.see"], uid, prisma))) {
return res.status(403).send({
error: "Unauthorized",
});
}
const permissionsRaw = await prisma.permission.findMany({
where: {
userID: uid,
},
});
return {
success: true,
// Get the ones that we have, and transform them into just their name
data: permissionsRaw.filter(i => i.has).map(i => i.permission),
};
},
);
}

View file

@ -0,0 +1,125 @@
import { hash } from "bcrypt";
import { permissionListEnabled } from "../../libs/permissions.js";
import { generateRandomData } from "../../libs/generateRandom.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens, options } = routeOptions;
/**
* Creates a new user account to use, only if it is enabled.
*/
fastify.post(
"/api/v1/users/create",
{
schema: {
body: {
type: "object",
required: ["name", "email", "username", "password"],
properties: {
name: { type: "string" },
username: { type: "string" },
email: { type: "string" },
password: { type: "string" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
name: string;
email: string;
password: string;
username: string;
} = req.body;
if (!options.isSignupEnabled) {
return res.status(403).send({
error: "Signing up is not enabled at this time.",
});
}
const userSearch = await prisma.user.findFirst({
where: {
email: body.email,
},
});
if (userSearch) {
return res.status(400).send({
error: "User already exists",
});
}
const saltedPassword: string = await hash(body.password, 15);
const userData = {
name: body.name,
email: body.email,
password: saltedPassword,
username: body.username,
permissions: {
create: [] as {
permission: string;
has: boolean;
}[],
},
};
// TODO: There's probably a faster way to pull this off, but I'm lazy
for (const permissionKey of Object.keys(permissionListEnabled)) {
if (
options.isSignupAsAdminEnabled ||
permissionKey.startsWith("routes") ||
permissionKey == "permissions.see"
) {
userData.permissions.create.push({
permission: permissionKey,
has: permissionListEnabled[permissionKey],
});
}
}
if (options.allowUnsafeGlobalTokens) {
// @ts-expect-error: Setting this correctly is a goddamn mess, but this is safe to an extent. It won't crash at least
userData.rootToken = generateRandomData();
// @ts-expect-error: Read above.
userData.isRootServiceAccount = true;
}
const userCreateResults = await prisma.user.create({
data: userData,
});
// FIXME(?): Redundant checks
if (options.allowUnsafeGlobalTokens) {
return {
success: true,
token: userCreateResults.rootToken,
};
} else {
const generatedToken = generateRandomData();
tokens[userCreateResults.id] = [];
tokens[userCreateResults.id].push({
createdAt: Date.now(),
expiresAt: Date.now() + 30 * 60_000,
token: generatedToken,
});
return {
success: true,
token: generatedToken,
};
}
},
);
}

View file

@ -0,0 +1,76 @@
import { compare } from "bcrypt";
import { generateRandomData } from "../../libs/generateRandom.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens } = routeOptions;
/**
* Logs in to a user account.
*/
fastify.post(
"/api/v1/users/login",
{
schema: {
body: {
type: "object",
required: ["password"],
properties: {
email: { type: "string" },
username: { type: "string" },
password: { type: "string" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
email?: string;
username?: string;
password: string;
} = req.body;
if (!body.email && !body.username)
return res.status(400).send({
error: "missing both email and username. please supply at least one.",
});
const userSearch = await prisma.user.findFirst({
where: {
email: body.email,
username: body.username,
},
});
if (!userSearch)
return res.status(403).send({
error: "Email or password is incorrect",
});
const passwordIsValid = await compare(body.password, userSearch.password);
if (!passwordIsValid)
return res.status(403).send({
error: "Email or password is incorrect",
});
const token = generateRandomData();
if (!tokens[userSearch.id]) tokens[userSearch.id] = [];
tokens[userSearch.id].push({
createdAt: Date.now(),
expiresAt: Date.now() + 30 * 60_000,
token,
});
return {
success: true,
token,
};
},
);
}

View file

@ -0,0 +1,72 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
fastify.post(
"/api/v1/users/lookup",
{
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
id: { type: "number" },
name: { type: "string" },
email: { type: "string" },
username: { type: "string" },
isServiceAccount: { type: "boolean" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
id?: number;
name?: string;
email?: string;
username?: string;
isServiceAccount?: boolean;
} = req.body;
if (!(await hasPermission(body.token, ["users.lookup"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
const users = await prisma.user.findMany({
where: {
id: body.id,
name: body.name,
email: body.email,
username: body.username,
isRootServiceAccount: body.isServiceAccount,
},
});
return {
success: true,
data: users.map(i => ({
id: i.id,
name: i.name,
email: i.email,
isServiceAccount: i.isRootServiceAccount,
username: i.username,
})),
};
},
);
}

View file

@ -0,0 +1,62 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const { fastify, prisma, tokens } = routeOptions;
function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
}
/**
* Creates a new backend to use
*/
fastify.post(
"/api/v1/users/remove",
{
schema: {
body: {
type: "object",
required: ["token", "uid"],
properties: {
token: { type: "string" },
uid: { type: "number" },
},
},
},
},
async (req, res) => {
// @ts-expect-error: Fastify routes schema parsing is trustworthy, so we can "assume" invalid types
const body: {
token: string;
uid: number;
} = req.body;
if (!(await hasPermission(body.token, ["users.remove"]))) {
return res.status(403).send({
error: "Unauthorized",
});
}
await prisma.permission.deleteMany({
where: {
userID: body.uid,
},
});
await prisma.user.delete({
where: {
id: body.uid,
},
});
return {
success: true,
};
},
);
}

View file

@ -0,0 +1,51 @@
import { createReadStream, createWriteStream } from "node:fs";
import { Readable, pipeline } from "node:stream";
import { createGzip } from "node:zlib";
import process from "node:process";
import { PrismaClient } from "@prisma/client";
const gzip = createGzip();
if (process.argv.length <= 2) {
console.error(
"Missing arguments! Usage: node ./out/tools/exportDBContents.js exportPath.json.gz",
);
process.exit(1);
}
console.log("Initializing Database...");
const prisma = new PrismaClient();
console.log("Initialized Database.");
console.log("Getting all destinationProviders...");
const destinationProviders = await prisma.desinationProvider.findMany();
console.log("Getting all forwardRules...");
const forwardRules = await prisma.forwardRule.findMany();
console.log("Getting all permissions...");
const allPermissions = await prisma.permission.findMany();
console.log("Getting all users...");
const users = await prisma.user.findMany();
const masterList = JSON.stringify({
destinationProviders,
forwardRules,
allPermissions,
});
const source = new Readable();
source.push(masterList);
source.push(null);
const destination = createWriteStream(process.argv[2]);
pipeline(source, gzip, destination, err => {
if (err) {
console.error("Failed to compress JSON data:", err);
} else {
console.log("Sucesfully saved DB contents.");
}
});

6
backend-legacy/srcpatch.sh Executable file
View file

@ -0,0 +1,6 @@
# !-- DO NOT USE THIS FOR DEVELOPMENT --!
# This is only to source patch files in production deployments, if prisma isn't configured already.
printf "//@ts-nocheck\n$(cat src/routes/backends/lookup.ts)" > src/routes/backends/lookup.ts
printf "//@ts-nocheck\n$(cat src/routes/forward/lookup.ts)" > src/routes/forward/lookup.ts
printf "//@ts-nocheck\n$(cat src/routes/user/lookup.ts)" > src/routes/user/lookup.ts
printf "//@ts-nocheck\n$(cat src/routes/getPermissions.ts)" > src/routes/getPermissions.ts

View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es2020",
"module": "es2022",
"moduleResolution": "node",
"outDir": "./out",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"strictPropertyInitialization": false,
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}