Merge branch 'dev' into reimpl-passyfire

This commit is contained in:
valerie 2024-05-05 17:01:53 -04:00 committed by GitHub
commit a18a770d5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1232 additions and 1029 deletions

2
.gitconfig Normal file
View file

@ -0,0 +1,2 @@
[core]
hooksPath = .githooks/

6
.githooks/pre-commit Executable file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
shopt -s globstar
"$(git rev-parse --show-toplevel)"/api/node_modules/.bin/prettier --ignore-unknown --write $(git rev-parse --show-toplevel)/api/src/**/*.ts
rustfmt $(git rev-parse --show-toplevel)/gui/src/**/*.rs
git update-index --again
exit 0

16
.prettierrc Normal file
View file

@ -0,0 +1,16 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "always",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

16
api/package-lock.json generated
View file

@ -21,6 +21,7 @@
"@types/ssh2": "^1.15.0", "@types/ssh2": "^1.15.0",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"nodemon": "^3.0.3", "nodemon": "^3.0.3",
"prettier": "^3.2.5",
"prisma": "^5.13.0", "prisma": "^5.13.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
@ -1288,6 +1289,21 @@
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz",
"integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="
}, },
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prisma": { "node_modules/prisma": {
"version": "5.13.0", "version": "5.13.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.13.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.13.0.tgz",

View file

@ -19,6 +19,7 @@
"@types/ssh2": "^1.15.0", "@types/ssh2": "^1.15.0",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"nodemon": "^3.0.3", "nodemon": "^3.0.3",
"prettier": "^3.2.5",
"prisma": "^5.13.0", "prisma": "^5.13.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },

View file

@ -1,19 +1,19 @@
export type ParameterReturnedValue = { export type ParameterReturnedValue = {
success: boolean, success: boolean;
message?: string message?: string;
} };
export type ForwardRule = { export type ForwardRule = {
sourceIP: string, sourceIP: string;
sourcePort: number, sourcePort: number;
destPort: number destPort: number;
}; };
export type ConnectedClient = { export type ConnectedClient = {
ip: string, ip: string;
port: number, port: number;
connectionDetails: ForwardRule connectionDetails: ForwardRule;
}; };
export class BackendBaseClass { export class BackendBaseClass {
@ -25,12 +25,22 @@ export class BackendBaseClass {
constructor(parameters: string) { constructor(parameters: string) {
this.logs = []; this.logs = [];
this.clients = []; this.clients = [];
this.state = "stopped"; this.state = "stopped";
} }
addConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {}; addConnection(
removeConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {}; 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> { async start(): Promise<boolean> {
return true; return true;
@ -38,22 +48,27 @@ export class BackendBaseClass {
async stop(): Promise<boolean> { async stop(): Promise<boolean> {
return true; return true;
}; }
getAllConnections(): ConnectedClient[] { getAllConnections(): ConnectedClient[] {
if (this.clients == null) return []; if (this.clients == null) return [];
return this.clients; return this.clients;
}; }
static checkParametersConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): ParameterReturnedValue { static checkParametersConnection(
sourceIP: string,
sourcePort: number,
destPort: number,
protocol: "tcp" | "udp",
): ParameterReturnedValue {
return { return {
success: true success: true,
} };
}; }
static checkParametersBackendInstance(data: string): ParameterReturnedValue { static checkParametersBackendInstance(data: string): ParameterReturnedValue {
return { return {
success: true success: true,
} };
}; }
} }

View file

@ -1,43 +1,51 @@
import { NodeSSH } from "node-ssh"; import { NodeSSH } from "node-ssh";
import { Socket } from "node:net"; import { Socket } from "node:net";
import type { BackendBaseClass, ForwardRule, ConnectedClient, ParameterReturnedValue } from "./base.js"; import type {
BackendBaseClass,
ForwardRule,
ConnectedClient,
ParameterReturnedValue,
} from "./base.js";
type ForwardRuleExt = ForwardRule & { type ForwardRuleExt = ForwardRule & {
enabled: boolean enabled: boolean;
} };
// Fight me (for better naming) // Fight me (for better naming)
type BackendParsedProviderString = { type BackendParsedProviderString = {
ip: string, ip: string;
port: number, port: number;
username: string, username: string;
privateKey: string privateKey: string;
} };
function parseBackendProviderString(data: string): BackendParsedProviderString { function parseBackendProviderString(data: string): BackendParsedProviderString {
try { try {
JSON.parse(data); JSON.parse(data);
} catch (e) { } catch (e) {
throw new Error("Payload body is not JSON") throw new Error("Payload body is not JSON");
} }
const jsonData = JSON.parse(data); const jsonData = JSON.parse(data);
if (typeof jsonData.ip != "string") throw new Error("IP field is not a string"); if (typeof jsonData.ip != "string")
throw new Error("IP field is not a string");
if (typeof jsonData.port != "number") throw new Error("Port is not a number"); if (typeof jsonData.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.username != "string")
if (typeof jsonData.privateKey != "string") throw new Error("Private key is not a string"); throw new Error("Username is not a string");
if (typeof jsonData.privateKey != "string")
throw new Error("Private key is not a string");
return { return {
ip: jsonData.ip, ip: jsonData.ip,
port: jsonData.port, port: jsonData.port,
username: jsonData.username, username: jsonData.username,
privateKey: jsonData.privateKey privateKey: jsonData.privateKey,
} };
} }
export class SSHBackendProvider implements BackendBaseClass { export class SSHBackendProvider implements BackendBaseClass {
@ -54,7 +62,7 @@ export class SSHBackendProvider implements BackendBaseClass {
this.logs = []; this.logs = [];
this.proxies = []; this.proxies = [];
this.clients = []; this.clients = [];
this.options = parseBackendProviderString(parameters); this.options = parseBackendProviderString(parameters);
this.state = "stopped"; this.state = "stopped";
@ -76,7 +84,7 @@ export class SSHBackendProvider implements BackendBaseClass {
port: this.options.port, port: this.options.port,
username: this.options.username, username: this.options.username,
privateKey: this.options.privateKey privateKey: this.options.privateKey,
}); });
} catch (e) { } catch (e) {
this.logs.push(`Failed to start SSHBackendProvider! Error: '${e}'`); this.logs.push(`Failed to start SSHBackendProvider! Error: '${e}'`);
@ -86,7 +94,7 @@ export class SSHBackendProvider implements BackendBaseClass {
this.sshInstance = null; this.sshInstance = null;
return false; return false;
}; }
this.state = "started"; this.state = "started";
this.logs.push("Successfully started SSHBackendProvider."); this.logs.push("Successfully started SSHBackendProvider.");
@ -109,57 +117,81 @@ export class SSHBackendProvider implements BackendBaseClass {
this.state = "stopped"; this.state = "stopped";
return true; return true;
}; }
addConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void { addConnection(
const connectionCheck = SSHBackendProvider.checkParametersConnection(sourceIP, sourcePort, destPort, protocol); 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); if (!connectionCheck.success) throw new Error(connectionCheck.message);
const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort); const foundProxyEntry = this.proxies.find(
i =>
i.sourceIP == sourceIP &&
i.sourcePort == sourcePort &&
i.destPort == destPort,
);
if (foundProxyEntry) return; if (foundProxyEntry) return;
(async() => { (async () => {
await this.sshInstance.forwardIn("0.0.0.0", destPort, (info, accept, reject) => { await this.sshInstance.forwardIn(
const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort); "0.0.0.0",
if (!foundProxyEntry || !foundProxyEntry.enabled) return reject(); destPort,
(info, accept, reject) => {
const foundProxyEntry = this.proxies.find(
i =>
i.sourceIP == sourceIP &&
i.sourcePort == sourcePort &&
i.destPort == destPort,
);
if (!foundProxyEntry || !foundProxyEntry.enabled) return reject();
const client: ConnectedClient = { const client: ConnectedClient = {
ip: info.srcIP, ip: info.srcIP,
port: info.srcPort, port: info.srcPort,
connectionDetails: foundProxyEntry connectionDetails: foundProxyEntry,
}; };
this.clients.push(client); this.clients.push(client);
const srcConn = new Socket();
srcConn.connect({
host: sourceIP,
port: sourcePort
});
// Why is this so confusing const srcConn = new Socket();
const destConn = accept();
destConn.addListener("data", (chunk: Uint8Array) => { srcConn.connect({
srcConn.write(chunk); host: sourceIP,
}); port: sourcePort,
});
destConn.addListener("close", () => { // Why is this so confusing
this.clients.splice(this.clients.indexOf(client), 1); const destConn = accept();
srcConn.end();
});
srcConn.on("data", (data) => { destConn.addListener("data", (chunk: Uint8Array) => {
destConn.write(data); srcConn.write(chunk);
}); });
srcConn.on("end", () => { destConn.addListener("close", () => {
this.clients.splice(this.clients.indexOf(client), 1); this.clients.splice(this.clients.indexOf(client), 1);
destConn.close(); srcConn.end();
}); });
});
srcConn.on("data", data => {
destConn.write(data);
});
srcConn.on("end", () => {
this.clients.splice(this.clients.indexOf(client), 1);
destConn.close();
});
},
);
})(); })();
this.proxies.push({ this.proxies.push({
@ -167,34 +199,56 @@ export class SSHBackendProvider implements BackendBaseClass {
sourcePort, sourcePort,
destPort, destPort,
enabled: true enabled: true,
}); });
}; }
removeConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void { removeConnection(
const connectionCheck = SSHBackendProvider.checkParametersConnection(sourceIP, sourcePort, destPort, protocol); 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); if (!connectionCheck.success) throw new Error(connectionCheck.message);
const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort); const foundProxyEntry = this.proxies.find(
i =>
i.sourceIP == sourceIP &&
i.sourcePort == sourcePort &&
i.destPort == destPort,
);
if (!foundProxyEntry) return; if (!foundProxyEntry) return;
foundProxyEntry.enabled = false; foundProxyEntry.enabled = false;
}; }
getAllConnections(): ConnectedClient[] { getAllConnections(): ConnectedClient[] {
return this.clients; return this.clients;
}; }
static checkParametersConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): ParameterReturnedValue { static checkParametersConnection(
if (protocol == "udp") return { sourceIP: string,
success: false, sourcePort: number,
message: "SSH does not support UDP tunneling! Please use something like PortCopier instead (if it gets done)" 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 { return {
success: true success: true,
} };
}; }
static checkParametersBackendInstance(data: string): ParameterReturnedValue { static checkParametersBackendInstance(data: string): ParameterReturnedValue {
try { try {
@ -203,12 +257,12 @@ export class SSHBackendProvider implements BackendBaseClass {
} catch (e: Error) { } catch (e: Error) {
return { return {
success: false, success: false,
message: e.toString() message: e.toString(),
} };
} }
return { return {
success: true success: true,
} };
}; }
} }

View file

@ -1,10 +1,14 @@
import process from "node:process"; import process from "node:process";
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from "@prisma/client";
import Fastify from "fastify"; import Fastify from "fastify";
import type { ServerOptions, SessionToken, RouteOptions } from "./libs/types.js"; import type {
import type { BackendBaseClass } from "./backendimpl/base.js";`` 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 getPermissions } from "./routes/getPermissions.js";
@ -28,20 +32,22 @@ import { backendInit } from "./libs/backendInit.js";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
const isSignupEnabled: boolean = Boolean(process.env.IS_SIGNUP_ENABLED); const isSignupEnabled: boolean = Boolean(process.env.IS_SIGNUP_ENABLED);
const unsafeAdminSignup: boolean = Boolean(process.env.UNSAFE_ADMIN_SIGNUP); const unsafeAdminSignup: boolean = Boolean(process.env.UNSAFE_ADMIN_SIGNUP);
const noUsersCheck: boolean = await prisma.user.count() == 0; const noUsersCheck: boolean = (await prisma.user.count()) == 0;
if (unsafeAdminSignup) { if (unsafeAdminSignup) {
console.error("WARNING: You have admin sign up on! This means that anyone that signs up will have admin rights!"); console.error(
"WARNING: You have admin sign up on! This means that anyone that signs up will have admin rights!",
);
} }
const serverOptions: ServerOptions = { const serverOptions: ServerOptions = {
isSignupEnabled: isSignupEnabled ? true : noUsersCheck, isSignupEnabled: isSignupEnabled ? true : noUsersCheck,
isSignupAsAdminEnabled: unsafeAdminSignup ? true : noUsersCheck, isSignupAsAdminEnabled: unsafeAdminSignup ? true : noUsersCheck,
allowUnsafeGlobalTokens: process.env.NODE_ENV != "production" allowUnsafeGlobalTokens: process.env.NODE_ENV != "production",
}; };
const sessionTokens: Record<number, SessionToken[]> = {}; const sessionTokens: Record<number, SessionToken[]> = {};
@ -58,7 +64,7 @@ const routeOptions: RouteOptions = {
tokens: sessionTokens, tokens: sessionTokens,
options: serverOptions, options: serverOptions,
backends: backends backends: backends,
}; };
console.log("Initializing forwarding rules..."); console.log("Initializing forwarding rules...");
@ -67,7 +73,7 @@ const createdBackends = await prisma.desinationProvider.findMany();
for (const backend of createdBackends) { for (const backend of createdBackends) {
console.log(`Running init steps for ID '${backend.id}' (${backend.name})`); console.log(`Running init steps for ID '${backend.id}' (${backend.name})`);
const init = await backendInit(backend, backends, prisma); const init = await backendInit(backend, backends, prisma);
if (init) console.log("Init successful."); if (init) console.log("Init successful.");
} }
@ -75,7 +81,7 @@ for (const backend of createdBackends) {
console.log("Done."); console.log("Done.");
getPermissions(routeOptions); getPermissions(routeOptions);
backendCreate(routeOptions); backendCreate(routeOptions);
backendRemove(routeOptions); backendRemove(routeOptions);
backendLookup(routeOptions); backendLookup(routeOptions);
@ -96,9 +102,9 @@ userLogin(routeOptions);
try { try {
await fastify.listen({ await fastify.listen({
port: 3000, port: 3000,
host: process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1" host: process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1",
}); });
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
process.exit(1); process.exit(1);
} }

View file

@ -5,15 +5,19 @@ import { backendProviders } from "../backendimpl/index.js";
type Backend = { type Backend = {
id: number; id: number;
name: string; name: string;
description: string | null; description: string | null;
backend: string; backend: string;
connectionDetails: string; connectionDetails: string;
}; };
export async function backendInit(backend: Backend, backends: Record<number, BackendBaseClass>, prisma: PrismaClient): Promise<boolean> { export async function backendInit(
backend: Backend,
backends: Record<number, BackendBaseClass>,
prisma: PrismaClient,
): Promise<boolean> {
const ourProvider = backendProviders[backend.backend]; const ourProvider = backendProviders[backend.backend];
if (!ourProvider) { if (!ourProvider) {
console.log(" - Error: Invalid backend recieved!"); console.log(" - Error: Invalid backend recieved!");
return false; return false;
@ -24,7 +28,7 @@ export async function backendInit(backend: Backend, backends: Record<number, Bac
backends[backend.id] = new ourProvider(backend.connectionDetails); backends[backend.id] = new ourProvider(backend.connectionDetails);
const ourBackend = backends[backend.id]; const ourBackend = backends[backend.id];
if (!await ourBackend.start()) { if (!(await ourBackend.start())) {
console.log(" - Error initializing backend!"); console.log(" - Error initializing backend!");
console.log(" - " + ourBackend.logs.join("\n - ")); console.log(" - " + ourBackend.logs.join("\n - "));
@ -36,18 +40,25 @@ export async function backendInit(backend: Backend, backends: Record<number, Bac
const clients = await prisma.forwardRule.findMany({ const clients = await prisma.forwardRule.findMany({
where: { where: {
destProviderID: backend.id, destProviderID: backend.id,
enabled: true enabled: true,
} },
}); });
for (const client of clients) { for (const client of clients) {
if (client.protocol != "tcp" && client.protocol != "udp") { if (client.protocol != "tcp" && client.protocol != "udp") {
console.error(` - Error: Client with ID of '${client.id}' has an invalid protocol! (must be either TCP or UDP)`); console.error(
` - Error: Client with ID of '${client.id}' has an invalid protocol! (must be either TCP or UDP)`,
);
continue; continue;
} }
ourBackend.addConnection(client.sourceIP, client.sourcePort, client.destPort, client.protocol); ourBackend.addConnection(
client.sourceIP,
client.sourcePort,
client.destPort,
client.protocol,
);
} }
return true; return true;
} }

View file

@ -9,7 +9,7 @@ export function generateRandomData(length: number = 128): string {
for (let i = 0; i < length; i += 2) { for (let i = 0; i < length; i += 2) {
const randomNumber = getRandomInt(0, 255); const randomNumber = getRandomInt(0, 255);
if (randomNumber == 0) { if (randomNumber == 0) {
i -= 2; i -= 2;
continue; continue;
@ -19,4 +19,4 @@ export function generateRandomData(length: number = 128): string {
} }
return newString; return newString;
} }

View file

@ -2,44 +2,50 @@ import type { PrismaClient } from "@prisma/client";
import type { SessionToken } from "./types.js"; import type { SessionToken } from "./types.js";
export const permissionListDisabled: Record<string, boolean> = { export const permissionListDisabled: Record<string, boolean> = {
"routes.add": false, "routes.add": false,
"routes.remove": false, "routes.remove": false,
"routes.start": false, "routes.start": false,
"routes.stop": false, "routes.stop": false,
"routes.edit": false, "routes.edit": false,
"routes.visible": false, "routes.visible": false,
"routes.visibleConn": false, "routes.visibleConn": false,
"backends.add": false, "backends.add": false,
"backends.remove": false, "backends.remove": false,
"backends.start": false, "backends.start": false,
"backends.stop": false, "backends.stop": false,
"backends.edit": false, "backends.edit": false,
"backends.visible": false, "backends.visible": false,
"backends.secretVis": false, "backends.secretVis": false,
"permissions.see": false, "permissions.see": false,
"users.add": false, "users.add": false,
"users.remove": false, "users.remove": false,
"users.lookup": false, "users.lookup": false,
"users.edit": false, "users.edit": false,
}; };
// FIXME: This solution fucking sucks. // FIXME: This solution fucking sucks.
export let permissionListEnabled: Record<string, boolean> = JSON.parse(JSON.stringify(permissionListDisabled)); export let permissionListEnabled: Record<string, boolean> = JSON.parse(
JSON.stringify(permissionListDisabled),
);
for (const index of Object.keys(permissionListEnabled)) { for (const index of Object.keys(permissionListEnabled)) {
permissionListEnabled[index] = true; permissionListEnabled[index] = true;
} }
export async function hasPermission(permissionList: string[], uid: number, prisma: PrismaClient): Promise<boolean> { export async function hasPermission(
permissionList: string[],
uid: number,
prisma: PrismaClient,
): Promise<boolean> {
for (const permission of permissionList) { for (const permission of permissionList) {
const permissionNode = await prisma.permission.findFirst({ const permissionNode = await prisma.permission.findFirst({
where: { where: {
userID: uid, userID: uid,
permission permission,
} },
}); });
if (!permissionNode || !permissionNode.has) return false; if (!permissionNode || !permissionNode.has) return false;
@ -48,7 +54,11 @@ export async function hasPermission(permissionList: string[], uid: number, prism
return true; return true;
} }
export async function getUID(token: string, tokens: Record<number, SessionToken[]>, prisma: PrismaClient): Promise<number> { export async function getUID(
token: string,
tokens: Record<number, SessionToken[]>,
prisma: PrismaClient,
): Promise<number> {
let userID = -1; let userID = -1;
// Look up in our currently authenticated users // Look up in our currently authenticated users
@ -59,7 +69,10 @@ export async function getUID(token: string, tokens: Record<number, SessionToken[
const otherToken = otherTokenList[otherTokenIndex]; const otherToken = otherTokenList[otherTokenIndex];
if (otherToken.token == token) { if (otherToken.token == token) {
if (otherToken.expiresAt < otherToken.createdAt + (otherToken.createdAt - Date.now())) { if (
otherToken.expiresAt <
otherToken.createdAt + (otherToken.createdAt - Date.now())
) {
otherTokenList.splice(parseInt(otherTokenIndex), 1); otherTokenList.splice(parseInt(otherTokenIndex), 1);
continue; continue;
} else { } else {
@ -74,19 +87,24 @@ export async function getUID(token: string, tokens: Record<number, SessionToken[
if (userID == -1) { if (userID == -1) {
const allUsers = await prisma.user.findMany({ const allUsers = await prisma.user.findMany({
where: { where: {
isRootServiceAccount: true isRootServiceAccount: true,
} },
}); });
for (const user of allUsers) { for (const user of allUsers) {
if (user.rootToken == token) userID = user.id; if (user.rootToken == token) userID = user.id;
}; }
} }
return userID; return userID;
} }
export async function hasPermissionByToken(permissionList: string[], token: string, tokens: Record<number, SessionToken[]>, prisma: PrismaClient): Promise<boolean> { export async function hasPermissionByToken(
permissionList: string[],
token: string,
tokens: Record<number, SessionToken[]>,
prisma: PrismaClient,
): Promise<boolean> {
const userID = await getUID(token, tokens, prisma); const userID = await getUID(token, tokens, prisma);
return await hasPermission(permissionList, userID, prisma); return await hasPermission(permissionList, userID, prisma);
} }

View file

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

View file

@ -5,90 +5,95 @@ import { backendProviders } from "../../backendimpl/index.js";
import { backendInit } from "../../libs/backendInit.js"; import { backendInit } from "../../libs/backendInit.js";
export function route(routeOptions: RouteOptions) { export function route(routeOptions: RouteOptions) {
const { const { fastify, prisma, tokens, backends } = routeOptions;
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> { function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma); return hasPermissionByToken(permissionList, token, tokens, prisma);
}; }
/** /**
* Creates a new backend to use * Creates a new backend to use
*/ */
fastify.post("/api/v1/backends/create", { fastify.post(
schema: { "/api/v1/backends/create",
body: { {
type: "object", schema: {
required: ["token", "name", "backend", "connectionDetails"], body: {
type: "object",
required: ["token", "name", "backend", "connectionDetails"],
properties: { properties: {
token: { type: "string" }, token: { type: "string" },
name: { type: "string" }, name: { type: "string" },
description: { type: "string" }, description: { type: "string" },
backend: { type: "string" } backend: { type: "string" },
} },
},
},
},
async (req, res) => {
// @ts-ignore
const body: {
token: string;
name: string;
description?: string;
connectionDetails: any;
backend: string;
} = req.body;
if (!(await hasPermission(body.token, ["backends.add"]))) {
return res.status(403).send({
error: "Unauthorized",
});
} }
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
name: string,
description?: string,
connectionDetails: any,
backend: string
} = req.body;
if (!await hasPermission(body.token, [ if (!backendProviders[body.backend]) {
"backends.add" return res.status(400).send({
])) { error: "Unknown/unsupported/deprecated backend!",
return res.status(403).send({ });
error: "Unauthorized"
});
};
if (!backendProviders[body.backend]) {
return res.status(400).send({
error: "Unknown/unsupported/deprecated backend!"
});
};
const connectionDetails = JSON.stringify(body.connectionDetails);
const connectionDetailsValidityCheck = backendProviders[body.backend].checkParametersBackendInstance(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: JSON.stringify(body.connectionDetails)
} }
});
const init = await backendInit(backend, backends, prisma); const connectionDetails = JSON.stringify(body.connectionDetails);
const connectionDetailsValidityCheck =
if (!init) { backendProviders[body.backend].checkParametersBackendInstance(
// TODO: better error code connectionDetails,
return res.status(504).send({ );
error: "Backend is created, but failed to initalize correctly",
id: backend.id 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: JSON.stringify(body.connectionDetails),
},
}); });
}
return { const init = await backendInit(backend, backends, prisma);
success: true,
id: backend.id 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

@ -2,77 +2,81 @@ import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js"; import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) { export function route(routeOptions: RouteOptions) {
const { const { fastify, prisma, tokens, backends } = routeOptions;
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> { function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma); return hasPermissionByToken(permissionList, token, tokens, prisma);
}; }
/** /**
* Creates a new route to use * Creates a new route to use
*/ */
fastify.post("/api/v1/backends/lookup", { fastify.post(
schema: { "/api/v1/backends/lookup",
body: { {
type: "object", schema: {
required: ["token"], body: {
type: "object",
required: ["token"],
properties: { properties: {
token: { type: "string" }, token: { type: "string" },
id: { type: "number" }, id: { type: "number" },
name: { type: "string" }, name: { type: "string" },
description: { type: "string" }, description: { type: "string" },
backend: { type: "string" } backend: { type: "string" },
} },
},
},
},
async (req, res) => {
// @ts-ignore
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",
});
} }
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id?: number,
name?: string,
description?: string,
backend?: string
} = req.body;
if (!await hasPermission(body.token, [ const canSeeSecrets = await hasPermission(body.token, [
"backends.visible" // wtf? "backends.secretVis",
])) { ]);
return res.status(403).send({
error: "Unauthorized" const prismaBackends = await prisma.desinationProvider.findMany({
where: {
id: body.id,
name: body.name,
description: body.description,
backend: body.backend,
},
}); });
};
const canSeeSecrets = await hasPermission(body.token, [ return {
"backends.secretVis" success: true,
]); data: prismaBackends.map(i => ({
name: i.name,
const prismaBackends = await prisma.desinationProvider.findMany({ description: i.description,
where: {
id: body.id,
name: body.name,
description: body.description,
backend: body.backend
}
});
return { backend: i.backend,
success: true, connectionDetails: canSeeSecrets ? i.connectionDetails : "",
data: prismaBackends.map((i) => ({
name: i.name,
description: i.description,
backend: i.backend, logs: backends[i.id].logs,
connectionDetails: canSeeSecrets ? i.connectionDetails : "", })),
};
logs: backends[i.id].logs },
})) );
} }
});
}

View file

@ -2,70 +2,70 @@ import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js"; import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) { export function route(routeOptions: RouteOptions) {
const { const { fastify, prisma, tokens, backends } = routeOptions;
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> { function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma); return hasPermissionByToken(permissionList, token, tokens, prisma);
}; }
/** /**
* Creates a new route to use * Creates a new route to use
*/ */
fastify.post("/api/v1/backends/remove", { fastify.post(
schema: { "/api/v1/backends/remove",
body: { {
type: "object", schema: {
required: ["token", "id"], body: {
type: "object",
required: ["token", "id"],
properties: { properties: {
token: { type: "string" }, token: { type: "string" },
id: { type: "number" } id: { type: "number" },
} },
},
},
},
async (req, res) => {
// @ts-ignore
const body: {
token: string;
id: number;
} = req.body;
if (!(await hasPermission(body.token, ["backends.remove"]))) {
return res.status(403).send({
error: "Unauthorized",
});
} }
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [ if (!backends[body.id]) {
"backends.remove" return res.status(400).send({
])) { error: "Backend not found",
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 { // Unload the backend
success: true 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

@ -2,61 +2,63 @@ import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js"; import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) { export function route(routeOptions: RouteOptions) {
const { const { fastify, prisma, tokens, backends } = routeOptions;
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> { function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma); return hasPermissionByToken(permissionList, token, tokens, prisma);
}; }
fastify.post("/api/v1/forward/connections", { fastify.post(
schema: { "/api/v1/forward/connections",
body: { {
type: "object", schema: {
required: ["token", "id"], body: {
type: "object",
required: ["token", "id"],
properties: { properties: {
token: { type: "string" }, token: { type: "string" },
id: { type: "number" } id: { type: "number" },
} },
},
},
},
async (req, res) => {
// @ts-ignore
const body: {
token: string;
id: number;
} = req.body;
if (!(await hasPermission(body.token, ["routes.visibleConn"]))) {
return res.status(403).send({
error: "Unauthorized",
});
} }
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [ const forward = await prisma.forwardRule.findUnique({
"routes.visibleConn" where: {
])) { id: body.id,
return res.status(403).send({ },
error: "Unauthorized"
}); });
};
const forward = await prisma.forwardRule.findUnique({ if (!forward)
where: { return res.status(400).send({
id: body.id error: "Could not find forward entry",
} });
});
if (!forward) return res.status(400).send({ if (!backends[forward.destProviderID])
error: "Could not find forward entry" return res.status(400).send({
}); error: "Backend not found",
});
if (!backends[forward.destProviderID]) return res.status(400).send({ return {
error: "Backend not found" success: true,
}); data: backends[forward.destProviderID].getAllConnections(),
};
return { },
success: true, );
data: backends[forward.destProviderID].getAllConnections() }
}
})
}

View file

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

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

@ -2,54 +2,55 @@ import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js"; import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) { export function route(routeOptions: RouteOptions) {
const { const { fastify, prisma, tokens } = routeOptions;
fastify,
prisma,
tokens
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> { function hasPermission(
token: string,
permissionList: string[],
): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma); return hasPermissionByToken(permissionList, token, tokens, prisma);
}; }
/** /**
* Creates a new route to use * Creates a new route to use
*/ */
fastify.post("/api/v1/forward/remove", { fastify.post(
schema: { "/api/v1/forward/remove",
body: { {
type: "object", schema: {
required: ["token", "id"], body: {
type: "object",
properties: { required: ["token", "id"],
token: { type: "string" },
id: { type: "number" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [ properties: {
"routes.remove" token: { type: "string" },
])) { id: { type: "number" },
return res.status(403).send({ },
error: "Unauthorized" },
},
},
async (req, res) => {
// @ts-ignore
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,
},
}); });
};
await prisma.forwardRule.delete({
where: {
id: body.id
}
});
return { return {
success: true success: true,
} };
}); },
} );
}

View file

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

View file

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

View file

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

@ -6,113 +6,116 @@ import { generateRandomData } from "../../libs/generateRandom.js";
import type { RouteOptions } from "../../libs/types.js"; import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) { export function route(routeOptions: RouteOptions) {
const { const { fastify, prisma, tokens, options } = routeOptions;
fastify,
prisma,
tokens,
options
} = routeOptions;
/** /**
* Creates a new user account to use, only if it is enabled. * Creates a new user account to use, only if it is enabled.
*/ */
fastify.post("/api/v1/users/create", { fastify.post(
schema: { "/api/v1/users/create",
body: { {
type: "object", schema: {
required: ["name", "email", "password"], body: {
type: "object",
required: ["name", "email", "password"],
properties: { properties: {
name: { type: "string" }, name: { type: "string" },
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
} },
} },
}, async(req, res) => { },
// @ts-ignore async (req, res) => {
const body: { // @ts-ignore
name: string, const body: {
email: string, name: string;
password: string email: string;
} = req.body; password: string;
} = req.body;
if (!options.isSignupEnabled) { if (!options.isSignupEnabled) {
return res.status(403).send({ return res.status(403).send({
error: "Signing up is not enabled at this time." 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,
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) { const userSearch = await prisma.user.findFirst({
// @ts-ignore where: {
userData.rootToken = generateRandomData(); email: body.email,
// @ts-ignore },
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 { if (userSearch) {
success: true, return res.status(400).send({
token: generatedToken error: "User already exists",
});
}
const saltedPassword: string = await hash(body.password, 15);
const userData = {
name: body.name,
email: body.email,
password: saltedPassword,
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-ignore
userData.rootToken = generateRandomData();
// @ts-ignore
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

@ -4,63 +4,65 @@ import { generateRandomData } from "../../libs/generateRandom.js";
import type { RouteOptions } from "../../libs/types.js"; import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) { export function route(routeOptions: RouteOptions) {
const { const { fastify, prisma, tokens } = routeOptions;
fastify,
prisma,
tokens
} = routeOptions;
/** /**
* Logs in to a user account. * Logs in to a user account.
*/ */
fastify.post("/api/v1/users/login", { fastify.post(
schema: { "/api/v1/users/login",
body: { {
type: "object", schema: {
required: ["email", "password"], body: {
type: "object",
required: ["email", "password"],
properties: { properties: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
} },
} },
}, async(req, res) => { },
// @ts-ignore async (req, res) => {
const body: { // @ts-ignore
email: string, const body: {
password: string email: string;
} = req.body; password: string;
} = req.body;
const userSearch = await prisma.user.findFirst({ const userSearch = await prisma.user.findFirst({
where: { where: {
email: body.email email: body.email,
} },
}); });
if (!userSearch) return res.status(403).send({ if (!userSearch)
error: "Email or password is incorrect" return res.status(403).send({
}); error: "Email or password is incorrect",
});
const passwordIsValid = await compare(body.password, userSearch.password); 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 (!passwordIsValid)
if (!tokens[userSearch.id]) tokens[userSearch.id] = []; return res.status(403).send({
error: "Email or password is incorrect",
tokens[userSearch.id].push({ });
createdAt: Date.now(),
expiresAt: Date.now() + (30 * 60_000),
token const token = generateRandomData();
}); if (!tokens[userSearch.id]) tokens[userSearch.id] = [];
return { tokens[userSearch.id].push({
success: true, createdAt: Date.now(),
token expiresAt: Date.now() + 30 * 60_000,
}
}); token,
} });
return {
success: true,
token,
};
},
);
}

View file

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

View file

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

View file

@ -3,17 +3,22 @@ use serde::Deserialize;
use serde_json::json; use serde_json::json;
pub struct NextAPIClient { pub struct NextAPIClient {
pub url: String pub url: String,
} }
#[derive(Deserialize, Debug, Default)] #[derive(Deserialize, Debug, Default)]
pub struct LoginResponse { pub struct LoginResponse {
pub error: Option<String>, pub error: Option<String>,
pub token: Option<String> pub token: Option<String>,
} }
impl NextAPIClient { impl NextAPIClient {
pub fn login(&self, email: &str, password: &str, mut callback: impl 'static + Send + FnMut(LoginResponse)) { pub fn login(
&self,
email: &str,
password: &str,
mut callback: impl 'static + Send + FnMut(LoginResponse),
) {
let json_data = json!({ let json_data = json!({
"email": email, "email": email,
"password": password "password": password
@ -24,7 +29,10 @@ impl NextAPIClient {
println!("{}", json_str); println!("{}", json_str);
let mut request = Request::post(self.url.clone() + "/api/v1/users/login", json_str.as_bytes().to_vec()); let mut request = Request::post(
self.url.clone() + "/api/v1/users/login",
json_str.as_bytes().to_vec(),
);
request.headers.insert("Content-Type", "application/json"); request.headers.insert("Content-Type", "application/json");
fetch(request, move |result: ehttp::Result<ehttp::Response>| { fetch(request, move |result: ehttp::Result<ehttp::Response>| {
@ -36,9 +44,7 @@ impl NextAPIClient {
} }
pub fn new(url: String) -> NextAPIClient { pub fn new(url: String) -> NextAPIClient {
let api_client: NextAPIClient = NextAPIClient { let api_client: NextAPIClient = NextAPIClient { url };
url
};
return api_client; return api_client;
} }

View file

@ -1,5 +1,5 @@
use std::sync::Arc;
use eframe::egui; use eframe::egui;
use std::sync::Arc;
use crate::api; use crate::api;
use crate::ApplicationState; use crate::ApplicationState;
@ -12,7 +12,7 @@ pub fn main(state: &mut ApplicationState, api: &api::NextAPIClient, ctx: &eframe
ui.label("Email: "); ui.label("Email: ");
ui.text_edit_singleline(&mut state.username); ui.text_edit_singleline(&mut state.username);
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
let label = ui.label("Password: "); let label = ui.label("Password: ");
ui.add(egui::TextEdit::singleline(&mut state.password).password(true)) ui.add(egui::TextEdit::singleline(&mut state.password).password(true))
@ -21,18 +21,20 @@ pub fn main(state: &mut ApplicationState, api: &api::NextAPIClient, ctx: &eframe
if ui.button("Login").clicked() { if ui.button("Login").clicked() {
let token_clone = Arc::clone(&state.token); let token_clone = Arc::clone(&state.token);
api.login(state.username.as_str(), state.password.as_str(), Box::new(move |res: api::LoginResponse| { api.login(
match res.token { state.username.as_str(),
state.password.as_str(),
Box::new(move |res: api::LoginResponse| match res.token {
Some(x) => { Some(x) => {
let mut token = token_clone.lock().unwrap(); let mut token = token_clone.lock().unwrap();
*token = x; *token = x;
}, }
None => { None => {
let mut token = token_clone.lock().unwrap(); let mut token = token_clone.lock().unwrap();
*token = "".to_string(); *token = "".to_string();
} }
} }),
})); );
} }
}); });
} }

View file

@ -1 +1 @@
pub mod log_in; pub mod log_in;

View file

@ -1,8 +1,8 @@
use std::sync::{Arc, Mutex};
use eframe::egui; use eframe::egui;
use std::sync::{Arc, Mutex};
mod components;
mod api; mod api;
mod components;
pub struct ApplicationState { pub struct ApplicationState {
token: Arc<Mutex<String>>, token: Arc<Mutex<String>>,
@ -20,21 +20,21 @@ fn main() -> Result<(), eframe::Error> {
let mut app_state: ApplicationState = ApplicationState { let mut app_state: ApplicationState = ApplicationState {
token: Arc::new(Mutex::new("".to_string())), token: Arc::new(Mutex::new("".to_string())),
// /!\ NOT THREAD SAFE FIELDS /!\ // /!\ NOT THREAD SAFE FIELDS /!\
// These are used internally for each application (immediate mode + functions which are stateless, // These are used internally for each application (immediate mode + functions which are stateless,
// and we need *a* state somehow) // and we need *a* state somehow)
// components/log_in.rs // components/log_in.rs
username: "replace@gmail.com".to_owned(), username: "replace@gmail.com".to_owned(),
password: "replace123".to_owned() password: "replace123".to_owned(),
}; };
eframe::run_simple_native("NextNet GUI", options, move |ctx, _frame| { eframe::run_simple_native("NextNet GUI", options, move |ctx, _frame| {
egui::CentralPanel::default().show(ctx, |_ui| { egui::CentralPanel::default().show(ctx, |_ui| {
let token_clone = Arc::clone(&app_state.token); let token_clone = Arc::clone(&app_state.token);
let token = token_clone.lock().unwrap(); let token = token_clone.lock().unwrap();
if *token == "".to_string() { if *token == "".to_string() {
components::log_in::main(&mut app_state, &api, ctx); components::log_in::main(&mut app_state, &api, ctx);
} else { } else {
@ -42,4 +42,4 @@ fn main() -> Result<(), eframe::Error> {
} }
}); });
}) })
} }

View file

@ -1,3 +1,5 @@
pushd api > /dev/null 2> /dev/null pushd api > /dev/null 2> /dev/null
source init.sh source init.sh
git config --local include.path .gitconfig
popd > /dev/null 2> /dev/null popd > /dev/null 2> /dev/null

View file

@ -5,6 +5,7 @@
# gui/ # gui/
cargo cargo
rustc rustc
rustfmt
# api/ # api/
nodejs nodejs