chore: Adds initial support for eslint.

This commit is contained in:
greysoh 2024-05-10 11:46:22 -04:00
parent 8b4f3715e9
commit 7e80b298a2
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
34 changed files with 3441 additions and 649 deletions

View file

@ -1,5 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
shopt -s globstar shopt -s globstar
set -e
pushd $(git rev-parse --show-toplevel)/api
npx eslint src
popd
pushd $(git rev-parse --show-toplevel)/lom
npx eslint src
popd
# Formatting step
"$(git rev-parse --show-toplevel)"/api/node_modules/.bin/prettier --ignore-unknown --write $(git rev-parse --show-toplevel)/{api,lom}/src/**/*.ts "$(git rev-parse --show-toplevel)"/api/node_modules/.bin/prettier --ignore-unknown --write $(git rev-parse --show-toplevel)/{api,lom}/src/**/*.ts
rustfmt $(git rev-parse --show-toplevel)/gui/src/**/*.rs rustfmt $(git rev-parse --show-toplevel)/gui/src/**/*.rs
git update-index --again git update-index --again

10
api/eslint.config.js Normal file
View file

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

1327
api/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,14 +14,18 @@
"author": "greysoh", "author": "greysoh",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.2.0",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/node": "^20.12.7", "@types/node": "^20.12.7",
"@types/ssh2": "^1.15.0", "@types/ssh2": "^1.15.0",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"eslint": "^8.57.0",
"globals": "^15.2.0",
"nodemon": "^3.0.3", "nodemon": "^3.0.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prisma": "^5.13.0", "prisma": "^5.13.0",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"typescript-eslint": "^7.8.0"
}, },
"dependencies": { "dependencies": {
"@fastify/websocket": "^10.0.1", "@fastify/websocket": "^10.0.1",

View file

@ -1,3 +1,5 @@
// @eslint-ignore-file
export type ParameterReturnedValue = { export type ParameterReturnedValue = {
success: boolean; success: boolean;
message?: string; message?: string;

View file

@ -10,4 +10,4 @@ export const backendProviders: Record<string, typeof BackendBaseClass> = {
if (process.env.NODE_ENV != "production") { if (process.env.NODE_ENV != "production") {
backendProviders["dummy"] = BackendBaseClass; backendProviders["dummy"] = BackendBaseClass;
} }

View file

@ -216,7 +216,7 @@ export class PassyFireBackendProvider implements BackendBaseClass {
static checkParametersBackendInstance(data: string): ParameterReturnedValue { static checkParametersBackendInstance(data: string): ParameterReturnedValue {
try { try {
parseBackendProviderString(data); parseBackendProviderString(data);
// @ts-ignore // @ts-expect-error
} catch (e: Error) { } catch (e: Error) {
return { return {
success: false, success: false,

View file

@ -69,7 +69,7 @@ export function route(instance: PassyFireBackendProvider) {
}, },
}, },
(req, res) => { (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
username: string; username: string;
password: string; password: string;
@ -115,7 +115,7 @@ export function route(instance: PassyFireBackendProvider) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
} = req.body; } = req.body;
@ -132,7 +132,7 @@ export function route(instance: PassyFireBackendProvider) {
req.hostname.indexOf(":") + 1, req.hostname.indexOf(":") + 1,
); );
// @ts-ignore // @ts-expect-error
// parseInt(...) can take a number just fine, at least in Node.JS // parseInt(...) can take a number just fine, at least in Node.JS
const port = parseInt(unparsedPort == "" ? proxiedPort : unparsedPort); const port = parseInt(unparsedPort == "" ? proxiedPort : unparsedPort);

View file

@ -57,7 +57,7 @@ export function requestHandler(
let state: "authentication" | "data" = "authentication"; let state: "authentication" | "data" = "authentication";
let socket: dgram.Socket | net.Socket | undefined; let socket: dgram.Socket | net.Socket | undefined;
// @ts-ignore // @ts-expect-error
let connectedClient: ConnectedClientExt = {}; let connectedClient: ConnectedClientExt = {};
ws.on("close", () => { ws.on("close", () => {

View file

@ -92,7 +92,7 @@ export class SSHBackendProvider implements BackendBaseClass {
this.logs.push(`Failed to start SSHBackendProvider! Error: '${e}'`); this.logs.push(`Failed to start SSHBackendProvider! Error: '${e}'`);
this.state = "stopped"; this.state = "stopped";
// @ts-ignore // @ts-expect-error
this.sshInstance = null; this.sshInstance = null;
return false; return false;
@ -112,7 +112,7 @@ export class SSHBackendProvider implements BackendBaseClass {
this.sshInstance.dispose(); this.sshInstance.dispose();
// @ts-ignore // @ts-expect-error
this.sshInstance = null; this.sshInstance = null;
this.logs.push("Successfully stopped SSHBackendProvider."); this.logs.push("Successfully stopped SSHBackendProvider.");
@ -255,7 +255,7 @@ export class SSHBackendProvider implements BackendBaseClass {
static checkParametersBackendInstance(data: string): ParameterReturnedValue { static checkParametersBackendInstance(data: string): ParameterReturnedValue {
try { try {
parseBackendProviderString(data); parseBackendProviderString(data);
// @ts-ignore // @ts-expect-error
} catch (e: Error) { } catch (e: Error) {
return { return {
success: false, success: false,

View file

@ -27,7 +27,7 @@ export const permissionListDisabled: Record<string, boolean> = {
}; };
// FIXME: This solution fucking sucks. // FIXME: This solution fucking sucks.
export let permissionListEnabled: Record<string, boolean> = JSON.parse( export const permissionListEnabled: Record<string, boolean> = JSON.parse(
JSON.stringify(permissionListDisabled), JSON.stringify(permissionListDisabled),
); );

View file

@ -35,7 +35,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
name: string; name: string;

View file

@ -33,7 +33,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
id?: number; id?: number;

View file

@ -30,7 +30,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
id: number; id: number;

View file

@ -27,7 +27,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
id: number; id: number;

View file

@ -50,7 +50,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;

View file

@ -41,7 +41,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;

View file

@ -30,7 +30,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
id: number; id: number;

View file

@ -30,7 +30,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
id: number; id: number;
@ -59,7 +59,7 @@ export function route(routeOptions: RouteOptions) {
}); });
// Other restrictions in place make it so that it MUST be either TCP or UDP // Other restrictions in place make it so that it MUST be either TCP or UDP
// @ts-ignore // @ts-expect-error
const protocol: "tcp" | "udp" = forward.protocol; const protocol: "tcp" | "udp" = forward.protocol;
backends[forward.destProviderID].addConnection( backends[forward.destProviderID].addConnection(

View file

@ -30,7 +30,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
id: number; id: number;
@ -59,7 +59,7 @@ export function route(routeOptions: RouteOptions) {
}); });
// Other restrictions in place make it so that it MUST be either TCP or UDP // Other restrictions in place make it so that it MUST be either TCP or UDP
// @ts-ignore // @ts-expect-error
const protocol: "tcp" | "udp" = forward.protocol; const protocol: "tcp" | "udp" = forward.protocol;
backends[forward.destProviderID].removeConnection( backends[forward.destProviderID].removeConnection(

View file

@ -22,7 +22,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
} = req.body; } = req.body;

View file

@ -29,7 +29,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
name: string; name: string;
email: string; email: string;
@ -87,9 +87,9 @@ export function route(routeOptions: RouteOptions) {
} }
if (options.allowUnsafeGlobalTokens) { if (options.allowUnsafeGlobalTokens) {
// @ts-ignore // @ts-expect-error
userData.rootToken = generateRandomData(); userData.rootToken = generateRandomData();
// @ts-ignore // @ts-expect-error
userData.isRootServiceAccount = true; userData.isRootServiceAccount = true;
} }

View file

@ -26,7 +26,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
email?: string; email?: string;
username?: string; username?: string;

View file

@ -31,7 +31,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
id?: number; id?: number;

View file

@ -30,7 +30,7 @@ export function route(routeOptions: RouteOptions) {
}, },
}, },
async (req, res) => { async (req, res) => {
// @ts-ignore // @ts-expect-error
const body: { const body: {
token: string; token: string;
uid: number; uid: number;

149
lom/diff.diff Normal file
View file

@ -0,0 +1,149 @@
diff --git a/api/src/backendimpl/passyfire-reimpl/routes.ts b/api/src/backendimpl/passyfire-reimpl/routes.ts
index 2961483..4519a87 100644
--- a/api/src/backendimpl/passyfire-reimpl/routes.ts
+++ b/api/src/backendimpl/passyfire-reimpl/routes.ts
@@ -47,25 +47,6 @@ export function route(instance: PassyFireBackendProvider) {
for (const spoofedRoute of unsupportedSpoofedRoutes) {
fastify.post(spoofedRoute, (req, res) => {
- if (typeof req.body != "string")
- return res.status(400).send({
- error: "Invalid token",
- });
-
- try {
- JSON.parse(req.body);
- } catch (e) {
- return res.status(400).send({
- error: "Invalid token",
- });
- }
-
- // @ts-expect-error
- if (!req.body.token)
- return res.status(400).send({
- error: "Invalid token",
- });
-
return res.status(403).send({
error: "Invalid scope(s)",
});
diff --git a/lom/src/commands/backends.ts b/lom/src/commands/backends.ts
index baba3d1..d16cac1 100644
--- a/lom/src/commands/backends.ts
+++ b/lom/src/commands/backends.ts
@@ -16,6 +16,18 @@ type BackendLookupSuccess = {
}[];
};
+const addRequiredOptions = {
+ ssh: [
+ "sshKey",
+ "username",
+ "host",
+ ],
+
+ passyfire: [
+ "host"
+ ]
+};
+
export async function run(
argv: string[],
println: PrintLine,
@@ -58,12 +70,12 @@ export async function run(
);
addBackend.option(
- "-u, --username",
+ "-u, --username <user>",
"(SSH, PassyFire) Username to authenticate with. With PassyFire, it's the username you create",
);
addBackend.option(
- "-h, --host",
+ "-h, --host <host>",
"(SSH, PassyFire) Host to connect to. With PassyFire, it's what you listen on",
);
@@ -86,10 +98,70 @@ export async function run(
);
addBackend.option(
- "-p, --password",
+ "-p, --password <password>",
"(PassyFire) What password you want to use for the primary user",
);
+ addBackend.action(async(name: string, provider: string, options: {
+ description?: string,
+ forceCustomParameters?: boolean,
+ customParameters?: string,
+
+ // SSH (mostly)
+ sshKey?: string,
+ username?: string,
+ host?: string,
+
+ // PassyFire (mostly)
+ isProxied?: boolean,
+ proxiedPortStr?: number,
+ guest?: boolean,
+ userAsk?: boolean,
+ password?: string
+ }) => {
+ // Yes it can index for what we need it to do.
+ // @ts-expect-error
+ const isUnsupportedPlatform: boolean = !addRequiredOptions[provider];
+
+ if (isUnsupportedPlatform) {
+ println("WARNING: Platform is not natively supported by the LOM yet!\n");
+ }
+
+ let connectionDetails: string = "";
+
+ if (options.forceCustomParameters || isUnsupportedPlatform) {
+ if (typeof options.customParameters != "string") {
+ return println("ERROR: You are missing the custom parameters option!\n");
+ }
+
+ connectionDetails = options.customParameters;
+ } else if (provider == "ssh") {
+ for (const argument of addRequiredOptions["ssh"]) {
+ // No.
+ // @ts-expect-error
+ const hasArgument = options[argument] as any;
+
+ if (!hasArgument) {
+ return println("ERROR: Missing argument '%s'\n", hasArgument);
+ };
+ };
+
+ // todo!
+ } else if (provider == "passyfire") {
+ for (const argument of addRequiredOptions["passyfire"]) {
+ // No.
+ // @ts-expect-error
+ const hasArgument = options[argument];
+
+ if (!hasArgument) {
+ return println("ERROR: Missing argument '%s'\n", hasArgument);
+ };
+ };
+
+ // todo!
+ }
+ });
+
const removeBackend = new SSHCommand(println, "rm");
removeBackend.description("Removes a backend");
removeBackend.argument("<id>", "ID of the backend");
@@ -269,7 +341,7 @@ export async function run(
// It would make sense to check this, then parse argv, however this causes issues with
// the application name not displaying correctly.
-
+
if (argv.length == 1) {
println("No arguments specified!\n\n");
program.help();

10
lom/eslint.config.js Normal file
View file

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

1243
lom/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,11 +14,15 @@
"author": "greysoh", "author": "greysoh",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.2.0",
"@types/node": "^20.12.8", "@types/node": "^20.12.8",
"@types/ssh2": "^1.15.0", "@types/ssh2": "^1.15.0",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.32",
"eslint": "^8.57.0",
"globals": "^15.2.0",
"nodemon": "^3.0.3", "nodemon": "^3.0.3",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"typescript-eslint": "^7.8.0"
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.8", "axios": "^1.6.8",

View file

@ -4,28 +4,22 @@ import { SSHCommand } from "../libs/patchCommander.js";
import type { PrintLine, KeyboardRead } from "../commands.js"; import type { PrintLine, KeyboardRead } from "../commands.js";
type BackendLookupSuccess = { type BackendLookupSuccess = {
success: boolean, success: boolean;
data: { data: {
id: number, id: number;
name: string, name: string;
description: string, description: string;
backend: string, backend: string;
connectionDetails?: string, connectionDetails?: string;
logs: string[] logs: string[];
}[]; }[];
}; };
const addRequiredOptions = { const addRequiredOptions = {
ssh: [ ssh: ["sshKey", "username", "host"],
"sshKey",
"username",
"host",
],
passyfire: [ passyfire: ["host"],
"host"
]
}; };
export async function run( export async function run(
@ -33,7 +27,7 @@ export async function run(
println: PrintLine, println: PrintLine,
axios: Axios, axios: Axios,
token: string, token: string,
readKeyboard: KeyboardRead readKeyboard: KeyboardRead,
) { ) {
const program = new SSHCommand(println); const program = new SSHCommand(println);
program.description("Manages backends for NextNet"); program.description("Manages backends for NextNet");
@ -103,218 +97,241 @@ export async function run(
"(PassyFire) What password you want to use for the primary user", "(PassyFire) What password you want to use for the primary user",
); );
addBackend.action(async(name: string, provider: string, options: { addBackend.action(
description?: string, async (
forceCustomParameters?: boolean, name: string,
customParameters?: string, provider: string,
options: {
// SSH (mostly) description?: string;
sshKey?: string, forceCustomParameters?: boolean;
username?: string, customParameters?: string;
host?: string,
// PassyFire (mostly) // SSH (mostly)
isProxied?: boolean, sshKey?: string;
proxiedPort?: string, username?: string;
guest?: boolean, host?: string;
userAsk?: boolean,
password?: string
}) => {
// Yes it can index for what we need it to do.
// @ts-ignore
const isUnsupportedPlatform: boolean = !addRequiredOptions[provider];
if (isUnsupportedPlatform) {
println("WARNING: Platform is not natively supported by the LOM yet!\n");
}
let connectionDetails: string = ""; // PassyFire (mostly)
isProxied?: boolean;
proxiedPort?: string;
guest?: boolean;
userAsk?: boolean;
password?: string;
},
) => {
// Yes it can index for what we need it to do.
// @ts-expect-error
const isUnsupportedPlatform: boolean = !addRequiredOptions[provider];
if (options.forceCustomParameters || isUnsupportedPlatform) { if (isUnsupportedPlatform) {
if (typeof options.customParameters != "string") { println(
return println("ERROR: You are missing the custom parameters option!\n"); "WARNING: Platform is not natively supported by the LOM yet!\n",
);
} }
connectionDetails = options.customParameters; let connectionDetails: string = "";
} else if (provider == "ssh") {
for (const argument of addRequiredOptions["ssh"]) {
// No.
// @ts-ignore
const hasArgument = options[argument] as any;
if (!hasArgument) {
return println("ERROR: Missing argument '%s'\n", argument);
};
};
const unstringifiedArguments: { if (options.forceCustomParameters || isUnsupportedPlatform) {
ip?: string, if (typeof options.customParameters != "string") {
port?: number, return println(
username?: string, "ERROR: You are missing the custom parameters option!\n",
privateKey?: string );
} = {};
if (options.host) {
const sourceSplit: string[] = options.host.split(":");
const sourceIP: string = sourceSplit[0];
const sourcePort: number = sourceSplit.length >= 2 ? parseInt(sourceSplit[1]) : 22;
unstringifiedArguments.ip = sourceIP;
unstringifiedArguments.port = sourcePort;
}
unstringifiedArguments.username = options.username;
unstringifiedArguments.privateKey = options.sshKey?.replaceAll("\\n", "\n");
connectionDetails = JSON.stringify(unstringifiedArguments);
} else if (provider == "passyfire") {
for (const argument of addRequiredOptions["passyfire"]) {
// No.
// @ts-ignore
const hasArgument = options[argument];
if (!hasArgument) {
return println("ERROR: Missing argument '%s'\n", argument);
};
};
const unstringifiedArguments: {
ip?: string,
port?: number,
publicPort?: number,
isProxied?: boolean,
users: {
username: string,
password: string
}[]
} = {
users: []
};
if (options.guest) {
unstringifiedArguments.users.push({
username: "guest",
password: "guest"
});
};
if (options.username) {
if (!options.password) {
return println("Password must not be left blank\n");
} }
unstringifiedArguments.users.push({ connectionDetails = options.customParameters;
username: options.username, } else if (provider == "ssh") {
password: options.password for (const argument of addRequiredOptions["ssh"]) {
}); // No.
}; // @ts-expect-error
const hasArgument = options[argument] as any;
if (options.userAsk) { if (!hasArgument) {
while (true) { return println("ERROR: Missing argument '%s'\n", argument);
println("Creating a user.\nUsername: "); }
const username = await readKeyboard(); }
let passwordConfirmOne = "a"; const unstringifiedArguments: {
let passwordConfirmTwo = "b"; ip?: string;
port?: number;
username?: string;
privateKey?: string;
} = {};
println("\n"); if (options.host) {
const sourceSplit: string[] = options.host.split(":");
while (passwordConfirmOne != passwordConfirmTwo) { const sourceIP: string = sourceSplit[0];
println("Password: "); const sourcePort: number =
passwordConfirmOne = await readKeyboard(true); sourceSplit.length >= 2 ? parseInt(sourceSplit[1]) : 22;
println("\nConfirm password: ");
passwordConfirmTwo = await readKeyboard(true);
println("\n"); unstringifiedArguments.ip = sourceIP;
unstringifiedArguments.port = sourcePort;
if (passwordConfirmOne != passwordConfirmTwo) { }
println("Passwords do not match! Try again.\n\n");
} unstringifiedArguments.username = options.username;
unstringifiedArguments.privateKey = options.sshKey?.replaceAll(
"\\n",
"\n",
);
connectionDetails = JSON.stringify(unstringifiedArguments);
} else if (provider == "passyfire") {
for (const argument of addRequiredOptions["passyfire"]) {
// No.
// @ts-expect-error
const hasArgument = options[argument];
if (!hasArgument) {
return println("ERROR: Missing argument '%s'\n", argument);
}
}
const unstringifiedArguments: {
ip?: string;
port?: number;
publicPort?: number;
isProxied?: boolean;
users: {
username: string;
password: string;
}[];
} = {
users: [],
};
if (options.guest) {
unstringifiedArguments.users.push({
username: "guest",
password: "guest",
});
}
if (options.username) {
if (!options.password) {
return println("Password must not be left blank\n");
} }
unstringifiedArguments.users.push({ unstringifiedArguments.users.push({
username, username: options.username,
password: passwordConfirmOne password: options.password,
}); });
println("\nShould we continue creating users? (y/n) ");
const shouldContinueAsking = (await readKeyboard()).toLowerCase().trim().startsWith("y");
println("\n\n");
if (!shouldContinueAsking) break;
}
}
if (unstringifiedArguments.users.length == 0) {
return println("No users will be created with your current arguments! You must have users set up.\n");
}
unstringifiedArguments.isProxied = Boolean(options.isProxied);
if (options.proxiedPort) {
unstringifiedArguments.publicPort = parseInt(options.proxiedPort ?? "");
if (Number.isNaN(unstringifiedArguments.publicPort)) {
println("UID (%s) is not a number.\n", options.proxiedPort);
return;
}
}
if (options.host) {
const sourceSplit: string[] = options.host.split(":");
if (sourceSplit.length != 2) {
return println("Source could not be splitted down (are you missing the ':' in the source to specify port?)\n");
} }
const sourceIP: string = sourceSplit[0]; if (options.userAsk) {
const sourcePort: number = parseInt(sourceSplit[1]); while (true) {
println("Creating a user.\nUsername: ");
const username = await readKeyboard();
if (Number.isNaN(sourcePort)) { let passwordConfirmOne = "a";
println("UID (%s) is not a number.\n", sourcePort); let passwordConfirmTwo = "b";
return;
println("\n");
while (passwordConfirmOne != passwordConfirmTwo) {
println("Password: ");
passwordConfirmOne = await readKeyboard(true);
println("\nConfirm password: ");
passwordConfirmTwo = await readKeyboard(true);
println("\n");
if (passwordConfirmOne != passwordConfirmTwo) {
println("Passwords do not match! Try again.\n\n");
}
}
unstringifiedArguments.users.push({
username,
password: passwordConfirmOne,
});
println("\nShould we continue creating users? (y/n) ");
const shouldContinueAsking = (await readKeyboard())
.toLowerCase()
.trim()
.startsWith("y");
println("\n\n");
if (!shouldContinueAsking) break;
}
} }
unstringifiedArguments.ip = sourceIP; if (unstringifiedArguments.users.length == 0) {
unstringifiedArguments.port = sourcePort; return println(
"No users will be created with your current arguments! You must have users set up.\n",
);
}
unstringifiedArguments.isProxied = Boolean(options.isProxied);
if (options.proxiedPort) {
unstringifiedArguments.publicPort = parseInt(
options.proxiedPort ?? "",
);
if (Number.isNaN(unstringifiedArguments.publicPort)) {
println("UID (%s) is not a number.\n", options.proxiedPort);
return;
}
}
if (options.host) {
const sourceSplit: string[] = options.host.split(":");
if (sourceSplit.length != 2) {
return println(
"Source could not be splitted down (are you missing the ':' in the source to specify port?)\n",
);
}
const sourceIP: string = sourceSplit[0];
const sourcePort: number = parseInt(sourceSplit[1]);
if (Number.isNaN(sourcePort)) {
println("UID (%s) is not a number.\n", sourcePort);
return;
}
unstringifiedArguments.ip = sourceIP;
unstringifiedArguments.port = sourcePort;
}
connectionDetails = JSON.stringify(unstringifiedArguments);
} }
connectionDetails = JSON.stringify(unstringifiedArguments); const response = await axios.post("/api/v1/backends/create", {
} token,
const response = await axios.post("/api/v1/backends/create", { name,
token, description: options.description,
backend: provider,
name,
description: options.description,
backend: provider,
connectionDetails connectionDetails,
}); });
if (response.status != 200) { if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response); if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) { if (response.data.error) {
println(`Error: ${response.data.error}\n`); println(`Error: ${response.data.error}\n`);
} else { } else {
println("Error creating a backend!\n"); println("Error creating a backend!\n");
}
return;
} }
return; println("Successfully created the backend.\n");
} },
);
println("Successfully created the backend.\n");
});
const removeBackend = new SSHCommand(println, "rm"); const removeBackend = new SSHCommand(println, "rm");
removeBackend.description("Removes a backend"); removeBackend.description("Removes a backend");
removeBackend.argument("<id>", "ID of the backend"); removeBackend.argument("<id>", "ID of the backend");
removeBackend.action(async(idStr: string) => { removeBackend.action(async (idStr: string) => {
const id: number = parseInt(idStr); const id: number = parseInt(idStr);
if (Number.isNaN(id)) { if (Number.isNaN(id)) {
@ -324,7 +341,7 @@ export async function run(
const response = await axios.post("/api/v1/backends/remove", { const response = await axios.post("/api/v1/backends/remove", {
token, token,
id id,
}); });
if (response.status != 200) { if (response.status != 200) {
@ -338,7 +355,7 @@ export async function run(
return; return;
} }
println("Backend has been successfully deleted.\n"); println("Backend has been successfully deleted.\n");
}); });
@ -359,96 +376,100 @@ export async function run(
lookupBackend.option( lookupBackend.option(
"-e, --parse-connection-details", "-e, --parse-connection-details",
"If specified, we automatically parse the connection details to make them human readable, if standard JSON." "If specified, we automatically parse the connection details to make them human readable, if standard JSON.",
); );
lookupBackend.action(async(options: { lookupBackend.action(
name?: string, async (options: {
provider?: string, name?: string;
description?: string, provider?: string;
parseConnectionDetails?: boolean description?: string;
}) => { parseConnectionDetails?: boolean;
const response = await axios.post("/api/v1/backends/lookup", { }) => {
token, const response = await axios.post("/api/v1/backends/lookup", {
token,
name: options.name,
description: options.description,
backend: options.provider name: options.name,
}); description: options.description,
if (response.status != 200) { backend: options.provider,
if (process.env.NODE_ENV != "production") console.log(response); });
if (response.data.error) { if (response.status != 200) {
println(`Error: ${response.data.error}\n`); if (process.env.NODE_ENV != "production") console.log(response);
} else {
println("Error looking up backends!\n");
}
return; if (response.data.error) {
} println(`Error: ${response.data.error}\n`);
const { data }: BackendLookupSuccess = response.data;
for (const backend of data) {
println("ID: %s:\n", backend.id);
println(" - Name: %s\n", backend.name);
println(" - Description: %s\n", backend.description);
println(" - Using Backend: %s\n", backend.backend);
if (backend.connectionDetails) {
if (options.parseConnectionDetails) {
// We don't know what we're recieving. We just try to parse it (hence the any type)
// {} is more accurate but TS yells at us if we do that :(
let parsedJSONData: any | undefined;
try {
parsedJSONData = JSON.parse(backend.connectionDetails);
} catch (e) {
println(" - Connection Details: %s\n", backend.connectionDetails);
continue;
}
if (!parsedJSONData) {
// Not really an assertion but I don't care right now
println("Assertion failed: parsedJSONData should not be undefined\n");
continue;
}
println(" - Connection details:\n");
for (const key of Object.keys(parsedJSONData)) {
let value: string | number = parsedJSONData[key];
if (typeof value == "string") {
value = value.replaceAll("\n", "\n" + " ".repeat(16));
}
if (typeof value == "object") {
// TODO: implement?
value = JSON.stringify(value);
}
println(" - %s: %s\n", key, value);
}
} else { } else {
println(" - Connection Details: %s\n", backend.connectionDetails); println("Error looking up backends!\n");
} }
return;
} }
println("\n"); const { data }: BackendLookupSuccess = response.data;
}
println("%s backends found.\n", data.length); for (const backend of data) {
}); println("ID: %s:\n", backend.id);
println(" - Name: %s\n", backend.name);
println(" - Description: %s\n", backend.description);
println(" - Using Backend: %s\n", backend.backend);
if (backend.connectionDetails) {
if (options.parseConnectionDetails) {
// We don't know what we're recieving. We just try to parse it (hence the any type)
// {} is more accurate but TS yells at us if we do that :(
let parsedJSONData: any | undefined;
try {
parsedJSONData = JSON.parse(backend.connectionDetails);
} catch (e) {
println(" - Connection Details: %s\n", backend.connectionDetails);
continue;
}
if (!parsedJSONData) {
// Not really an assertion but I don't care right now
println(
"Assertion failed: parsedJSONData should not be undefined\n",
);
continue;
}
println(" - Connection details:\n");
for (const key of Object.keys(parsedJSONData)) {
let value: string | number = parsedJSONData[key];
if (typeof value == "string") {
value = value.replaceAll("\n", "\n" + " ".repeat(16));
}
if (typeof value == "object") {
// TODO: implement?
value = JSON.stringify(value);
}
println(" - %s: %s\n", key, value);
}
} else {
println(" - Connection Details: %s\n", backend.connectionDetails);
}
}
println("\n");
}
println("%s backends found.\n", data.length);
},
);
const logsCommand = new SSHCommand(println, "logs"); const logsCommand = new SSHCommand(println, "logs");
logsCommand.description("View logs for a backend"); logsCommand.description("View logs for a backend");
logsCommand.argument("<id>", "ID of the backend"); logsCommand.argument("<id>", "ID of the backend");
logsCommand.action(async(idStr: string) => { logsCommand.action(async (idStr: string) => {
const id: number = parseInt(idStr); const id: number = parseInt(idStr);
if (Number.isNaN(id)) { if (Number.isNaN(id)) {
@ -458,7 +479,7 @@ export async function run(
const response = await axios.post("/api/v1/backends/lookup", { const response = await axios.post("/api/v1/backends/lookup", {
token, token,
id id,
}); });
if (response.status != 200) { if (response.status != 200) {
@ -474,10 +495,10 @@ export async function run(
} }
const { data }: BackendLookupSuccess = response.data; const { data }: BackendLookupSuccess = response.data;
const ourBackend = data.find((i) => i.id == id); const ourBackend = data.find(i => i.id == id);
if (!ourBackend) return println("Could not find the backend!\n"); if (!ourBackend) return println("Could not find the backend!\n");
ourBackend.logs.forEach((log) => println("%s\n", log)); ourBackend.logs.forEach(log => println("%s\n", log));
}); });
program.addCommand(addBackend); program.addCommand(addBackend);
@ -496,5 +517,5 @@ export async function run(
return; return;
} }
await new Promise((resolve) => program.onExit(resolve)); await new Promise(resolve => program.onExit(resolve));
} }

View file

@ -5,36 +5,36 @@ import type { PrintLine } from "../commands.js";
// https://stackoverflow.com/questions/37938504/what-is-the-best-way-to-find-all-items-are-deleted-inserted-from-original-arra // https://stackoverflow.com/questions/37938504/what-is-the-best-way-to-find-all-items-are-deleted-inserted-from-original-arra
function difference(a: any[], b: any[]) { function difference(a: any[], b: any[]) {
return a.filter(x => b.indexOf(x) < 0); return a.filter(x => b.indexOf(x) < 0);
}; }
type InboundConnectionSuccess = { type InboundConnectionSuccess = {
success: true, success: true;
data: { data: {
ip: string, ip: string;
port: number, port: number;
connectionDetails: { connectionDetails: {
sourceIP: string, sourceIP: string;
sourcePort: number, sourcePort: number;
destPort: number, destPort: number;
enabled: boolean enabled: boolean;
} };
}[] }[];
}; };
type LookupCommandSuccess = { type LookupCommandSuccess = {
success: true, success: true;
data: { data: {
id: number, id: number;
name: string, name: string;
description: string, description: string;
sourceIP: string, sourceIP: string;
sourcePort: number, sourcePort: number;
destPort: number, destPort: number;
providerID: number, providerID: number;
autoStart: boolean autoStart: boolean;
}[] }[];
}; };
export async function run( export async function run(
@ -43,7 +43,11 @@ export async function run(
axios: Axios, axios: Axios,
token: string, token: string,
) { ) {
if (argv.length == 1) return println("error: no arguments specified! run %s --help to see commands.\n", argv[0]); if (argv.length == 1)
return println(
"error: no arguments specified! run %s --help to see commands.\n",
argv[0],
);
const program = new SSHCommand(println); const program = new SSHCommand(println);
program.description("Manages connections for NextNet"); program.description("Manages connections for NextNet");
@ -68,71 +72,82 @@ export async function run(
addCommand.argument("<dest_port>", "Destination port to use"); addCommand.argument("<dest_port>", "Destination port to use");
addCommand.option("-d, --description", "Description for the tunnel"); addCommand.option("-d, --description", "Description for the tunnel");
addCommand.action(async(providerIDStr: string, name: string, protocolRaw: string, source: string, destPortRaw: string, options: { addCommand.action(
description?: string async (
}) => { providerIDStr: string,
const providerID = parseInt(providerIDStr); name: string,
protocolRaw: string,
source: string,
destPortRaw: string,
options: {
description?: string;
},
) => {
const providerID = parseInt(providerIDStr);
if (Number.isNaN(providerID)) { if (Number.isNaN(providerID)) {
println("ID (%s) is not a number\n", providerIDStr); println("ID (%s) is not a number\n", providerIDStr);
return; return;
};
const protocol = protocolRaw.toLowerCase().trim();
if (protocol != "tcp" && protocol != "udp") {
return println("Protocol is not a valid option (not tcp or udp)\n");
};
const sourceSplit: string[] = source.split(":");
if (sourceSplit.length != 2) {
return println("Source could not be splitted down (are you missing the ':' in the source to specify port?)\n");
}
const sourceIP: string = sourceSplit[0];
const sourcePort: number = parseInt(sourceSplit[1]);
if (Number.isNaN(sourcePort)) {
return println("Port splitted is not a number\n");
}
const destinationPort: number = parseInt(destPortRaw);
if (Number.isNaN(destinationPort)) {
return println("Destination port could not be parsed into a number\n");
}
const response = await axios.post("/api/v1/forward/create", {
token,
name,
description: options.description,
protocol,
sourceIP,
sourcePort,
destinationPort,
providerID
});
if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else {
println("Error creating a connection!\n");
} }
return; const protocol = protocolRaw.toLowerCase().trim();
}
println("Successfully created connection.\n"); if (protocol != "tcp" && protocol != "udp") {
}); return println("Protocol is not a valid option (not tcp or udp)\n");
}
const sourceSplit: string[] = source.split(":");
if (sourceSplit.length != 2) {
return println(
"Source could not be splitted down (are you missing the ':' in the source to specify port?)\n",
);
}
const sourceIP: string = sourceSplit[0];
const sourcePort: number = parseInt(sourceSplit[1]);
if (Number.isNaN(sourcePort)) {
return println("Port splitted is not a number\n");
}
const destinationPort: number = parseInt(destPortRaw);
if (Number.isNaN(destinationPort)) {
return println("Destination port could not be parsed into a number\n");
}
const response = await axios.post("/api/v1/forward/create", {
token,
name,
description: options.description,
protocol,
sourceIP,
sourcePort,
destinationPort,
providerID,
});
if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else {
println("Error creating a connection!\n");
}
return;
}
println("Successfully created connection.\n");
},
);
const lookupCommand = new SSHCommand(println, "find"); const lookupCommand = new SSHCommand(println, "find");
@ -164,111 +179,124 @@ export async function run(
"Description for the tunnel", "Description for the tunnel",
); );
lookupCommand.action(async(options: { lookupCommand.action(
backendId?: string, async (options: {
destPort?: string, backendId?: string;
name?: string, destPort?: string;
protocol?: string, name?: string;
source?: string, protocol?: string;
description?: string source?: string;
}) => { description?: string;
let numberBackendID: number | undefined; }) => {
let numberBackendID: number | undefined;
let sourceIP: string | undefined; let sourceIP: string | undefined;
let sourcePort: number | undefined; let sourcePort: number | undefined;
let destPort: number | undefined; let destPort: number | undefined;
if (options.backendId) { if (options.backendId) {
numberBackendID = parseInt(options.backendId); numberBackendID = parseInt(options.backendId);
if (Number.isNaN(numberBackendID)) {
println("ID (%s) is not a number\n", options.backendId);
return;
}
}
if (options.source) {
const sourceSplit: string[] = options.source.split(":");
if (sourceSplit.length != 2) {
return println(
"Source could not be splitted down (are you missing the ':' in the source to specify port?)\n",
);
}
sourceIP = sourceSplit[0];
sourcePort = parseInt(sourceSplit[1]);
if (Number.isNaN(sourcePort)) {
return println("Port splitted is not a number\n");
}
}
if (options.destPort) {
destPort = parseInt(options.destPort);
if (Number.isNaN(destPort)) {
println("ID (%s) is not a number\n", options.destPort);
return;
}
}
const response = await axios.post("/api/v1/forward/lookup", {
token,
name: options.name,
description: options.description,
protocol: options.protocol,
sourceIP,
sourcePort,
destinationPort: destPort,
});
if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else {
println("Error requesting connections!\n");
}
if (Number.isNaN(numberBackendID)) {
println("ID (%s) is not a number\n", options.backendId);
return; return;
} }
}
if (options.source) { const { data }: LookupCommandSuccess = response.data;
const sourceSplit: string[] = options.source.split(":");
if (sourceSplit.length != 2) { for (const connection of data) {
return println("Source could not be splitted down (are you missing the ':' in the source to specify port?)\n"); println(
} "ID: %s%s:\n",
connection.id,
sourceIP = sourceSplit[0]; connection.autoStart ? " (automatically starts)" : "",
sourcePort = parseInt(sourceSplit[1]); );
println(" - Backend ID: %s\n", connection.providerID);
if (Number.isNaN(sourcePort)) { println(" - Name: %s\n", connection.name);
return println("Port splitted is not a number\n"); if (connection.description)
} println(" - Description: %s\n", connection.description);
} println(
" - Source: %s:%s\n",
connection.sourceIP,
connection.sourcePort,
);
println(" - Destination port: %s\n", connection.destPort);
if (options.destPort) { println("\n");
destPort = parseInt(options.destPort);
if (Number.isNaN(destPort)) {
println("ID (%s) is not a number\n", options.destPort);
return;
}
}
const response = await axios.post("/api/v1/forward/lookup", {
token,
name: options.name,
description: options.description,
protocol: options.protocol,
sourceIP,
sourcePort,
destinationPort: destPort
});
if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else {
println("Error requesting connections!\n");
} }
return; println("%s connections found.\n", data.length);
} },
);
const { data }: LookupCommandSuccess = response.data;
for (const connection of data) {
println("ID: %s%s:\n", connection.id, (connection.autoStart ? " (automatically starts)" : ""));
println(" - Backend ID: %s\n", connection.providerID);
println(" - Name: %s\n", connection.name);
if (connection.description) println(" - Description: %s\n", connection.description);
println(" - Source: %s:%s\n", connection.sourceIP, connection.sourcePort);
println(" - Destination port: %s\n", connection.destPort);
println("\n");
}
println("%s connections found.\n", data.length);
});
const startTunnel = new SSHCommand(println, "start"); const startTunnel = new SSHCommand(println, "start");
startTunnel.description("Starts a tunnel"); startTunnel.description("Starts a tunnel");
startTunnel.argument("<id>", "Tunnel ID to start"); startTunnel.argument("<id>", "Tunnel ID to start");
startTunnel.action(async(idStr: string) => { startTunnel.action(async (idStr: string) => {
const id = parseInt(idStr); const id = parseInt(idStr);
if (Number.isNaN(id)) { if (Number.isNaN(id)) {
println("ID (%s) is not a number\n", idStr); println("ID (%s) is not a number\n", idStr);
return; return;
}; }
const response = await axios.post("/api/v1/forward/start", { const response = await axios.post("/api/v1/forward/start", {
token, token,
id id,
}); });
if (response.status != 200) { if (response.status != 200) {
@ -291,17 +319,17 @@ export async function run(
stopTunnel.description("Stops a tunnel"); stopTunnel.description("Stops a tunnel");
stopTunnel.argument("<id>", "Tunnel ID to stop"); stopTunnel.argument("<id>", "Tunnel ID to stop");
stopTunnel.action(async(idStr: string) => { stopTunnel.action(async (idStr: string) => {
const id = parseInt(idStr); const id = parseInt(idStr);
if (Number.isNaN(id)) { if (Number.isNaN(id)) {
println("ID (%s) is not a number\n", idStr); println("ID (%s) is not a number\n", idStr);
return; return;
}; }
const response = await axios.post("/api/v1/forward/stop", { const response = await axios.post("/api/v1/forward/stop", {
token, token,
id id,
}); });
if (response.status != 200) { if (response.status != 200) {
@ -323,107 +351,127 @@ export async function run(
getInbound.description("Shows all current connections"); getInbound.description("Shows all current connections");
getInbound.argument("<id>", "Tunnel ID to view inbound connections of"); getInbound.argument("<id>", "Tunnel ID to view inbound connections of");
getInbound.option("-t, --tail", "Live-view of connection list"); getInbound.option("-t, --tail", "Live-view of connection list");
getInbound.option("-s, --tail-pull-rate <ms>", "Controls the speed to pull at (in ms)"); getInbound.option(
"-s, --tail-pull-rate <ms>",
"Controls the speed to pull at (in ms)",
);
getInbound.action(async(idStr: string, options: { getInbound.action(
tail?: boolean, async (
tailPullRate?: string idStr: string,
}): Promise<void> => { options: {
const pullRate: number = options.tailPullRate ? parseInt(options.tailPullRate) : 2000; tail?: boolean;
const id = parseInt(idStr); tailPullRate?: string;
},
): Promise<void> => {
const pullRate: number = options.tailPullRate
? parseInt(options.tailPullRate)
: 2000;
const id = parseInt(idStr);
if (Number.isNaN(id)) { if (Number.isNaN(id)) {
println("ID (%s) is not a number\n", idStr); println("ID (%s) is not a number\n", idStr);
return; return;
} }
if (Number.isNaN(pullRate)) { if (Number.isNaN(pullRate)) {
println("Pull rate is not a number\n"); println("Pull rate is not a number\n");
return; return;
} }
if (options.tail) { if (options.tail) {
let previousEntries: string[] = []; let previousEntries: string[] = [];
while (true) { while (true) {
const response = await axios.post("/api/v1/forward/connections", {
token,
id,
});
if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else {
println("Error requesting inbound connections!\n");
}
return;
}
const { data }: InboundConnectionSuccess = response.data;
const simplifiedArray: string[] = data.map(i => `${i.ip}:${i.port}`);
const insertedItems: string[] = difference(
simplifiedArray,
previousEntries,
);
const removedItems: string[] = difference(
previousEntries,
simplifiedArray,
);
insertedItems.forEach(i => println("CONNECTED: %s\n", i));
removedItems.forEach(i => println("DISCONNECTED: %s\n", i));
previousEntries = simplifiedArray;
await new Promise(i => setTimeout(i, pullRate));
}
} else {
const response = await axios.post("/api/v1/forward/connections", { const response = await axios.post("/api/v1/forward/connections", {
token, token,
id id,
}); });
if (response.status != 200) { if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response); if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) { if (response.data.error) {
println(`Error: ${response.data.error}\n`); println(`Error: ${response.data.error}\n`);
} else { } else {
println("Error requesting inbound connections!\n"); println("Error requesting connections!\n");
} }
return; return;
} }
const { data }: InboundConnectionSuccess = response.data; const { data }: InboundConnectionSuccess = response.data;
const simplifiedArray: string[] = data.map((i) => `${i.ip}:${i.port}`);
const insertedItems: string[] = difference(simplifiedArray, previousEntries); if (data.length == 0) {
const removedItems: string[] = difference(previousEntries, simplifiedArray); println("There are currently no connected clients.\n");
return;
insertedItems.forEach((i) => println("CONNECTED: %s\n", i));
removedItems.forEach((i) => println("DISCONNECTED: %s\n", i));
previousEntries = simplifiedArray;
await new Promise((i) => setTimeout(i, pullRate));
}
} else {
const response = await axios.post("/api/v1/forward/connections", {
token,
id
});
if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else {
println("Error requesting connections!\n");
} }
return; println(
"Connected clients (for source: %s:%s):\n",
data[0].connectionDetails.sourceIP,
data[0].connectionDetails.sourcePort,
);
for (const entry of data) {
println(" - %s:%s\n", entry.ip, entry.port);
}
} }
},
const { data }: InboundConnectionSuccess = response.data; );
if (data.length == 0) {
println("There are currently no connected clients.\n");
return;
}
println("Connected clients (for source: %s:%s):\n", data[0].connectionDetails.sourceIP, data[0].connectionDetails.sourcePort);
for (const entry of data) {
println(" - %s:%s\n", entry.ip, entry.port);
}
}
});
const removeTunnel = new SSHCommand(println, "rm"); const removeTunnel = new SSHCommand(println, "rm");
removeTunnel.description("Removes a tunnel"); removeTunnel.description("Removes a tunnel");
removeTunnel.argument("<id>", "Tunnel ID to remove"); removeTunnel.argument("<id>", "Tunnel ID to remove");
removeTunnel.action(async(idStr: string) => { removeTunnel.action(async (idStr: string) => {
const id = parseInt(idStr); const id = parseInt(idStr);
if (Number.isNaN(id)) { if (Number.isNaN(id)) {
println("ID (%s) is not a number\n", idStr); println("ID (%s) is not a number\n", idStr);
return; return;
}; }
const response = await axios.post("/api/v1/forward/remove", { const response = await axios.post("/api/v1/forward/remove", {
token, token,
id id,
}); });
if (response.status != 200) { if (response.status != 200) {
@ -436,7 +484,7 @@ export async function run(
} }
return; return;
}; }
println("Successfully deleted connection.\n"); println("Successfully deleted connection.\n");
}); });
@ -449,5 +497,5 @@ export async function run(
program.addCommand(removeTunnel); program.addCommand(removeTunnel);
program.parse(argv); program.parse(argv);
await new Promise((resolve) => program.onExit(resolve)); await new Promise(resolve => program.onExit(resolve));
} }

View file

@ -6,11 +6,11 @@ import type { PrintLine, KeyboardRead } from "../commands.js";
type UserLookupSuccess = { type UserLookupSuccess = {
success: true; success: true;
data: { data: {
id: number, id: number;
isServiceAccount: boolean, isServiceAccount: boolean;
username: string, username: string;
name: string, name: string;
email: string email: string;
}[]; }[];
}; };
@ -19,9 +19,13 @@ export async function run(
println: PrintLine, println: PrintLine,
axios: Axios, axios: Axios,
apiKey: string, apiKey: string,
readKeyboard: KeyboardRead readKeyboard: KeyboardRead,
) { ) {
if (argv.length == 1) return println("error: no arguments specified! run %s --help to see commands.\n", argv[0]); if (argv.length == 1)
return println(
"error: no arguments specified! run %s --help to see commands.\n",
argv[0],
);
const program = new SSHCommand(println); const program = new SSHCommand(println);
program.description("Manages users for NextNet"); program.description("Manages users for NextNet");
@ -39,69 +43,76 @@ export async function run(
"Asks for a password. Hides output", "Asks for a password. Hides output",
); );
addCommand.action(async(username: string, email: string, name: string, options: { addCommand.action(
password?: string, async (
askPassword?: boolean username: string,
}) => { email: string,
if (!options.password && !options.askPassword) { name: string,
println("No password supplied, and askpass has not been supplied.\n"); options: {
return; password?: string;
}; askPassword?: boolean;
},
) => {
if (!options.password && !options.askPassword) {
println("No password supplied, and askpass has not been supplied.\n");
return;
}
let password: string = ""; let password: string = "";
if (options.askPassword) { if (options.askPassword) {
let passwordConfirmOne = "a"; let passwordConfirmOne = "a";
let passwordConfirmTwo = "b"; let passwordConfirmTwo = "b";
while (passwordConfirmOne != passwordConfirmTwo) { while (passwordConfirmOne != passwordConfirmTwo) {
println("Password: "); println("Password: ");
passwordConfirmOne = await readKeyboard(true); passwordConfirmOne = await readKeyboard(true);
println("\nConfirm password: ");
passwordConfirmTwo = await readKeyboard(true);
println("\n"); println("\nConfirm password: ");
passwordConfirmTwo = await readKeyboard(true);
if (passwordConfirmOne != passwordConfirmTwo) { println("\n");
println("Passwords do not match! Try again.\n\n");
if (passwordConfirmOne != passwordConfirmTwo) {
println("Passwords do not match! Try again.\n\n");
}
} }
}
password = passwordConfirmOne; password = passwordConfirmOne;
} else {
// From the first check we do, we know this is safe (you MUST specify a password)
// @ts-ignore
password = options.password;
}
const response = await axios.post("/api/v1/users/create", {
name,
username,
email,
password
});
if (response.status != 200) {
if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else { } else {
println("Error creating users!\n"); // From the first check we do, we know this is safe (you MUST specify a password)
// @ts-expect-error
password = options.password;
} }
return; const response = await axios.post("/api/v1/users/create", {
} name,
username,
email,
password,
});
println("User created successfully.\n"); if (response.status != 200) {
}) if (process.env.NODE_ENV != "production") console.log(response);
if (response.data.error) {
println(`Error: ${response.data.error}\n`);
} else {
println("Error creating users!\n");
}
return;
}
println("User created successfully.\n");
},
);
const removeCommand = new SSHCommand(println, "rm"); const removeCommand = new SSHCommand(println, "rm");
removeCommand.description("Remove a user"); removeCommand.description("Remove a user");
removeCommand.argument("<uid>", "ID of user to remove"); removeCommand.argument("<uid>", "ID of user to remove");
removeCommand.action(async(uidStr: string) => { removeCommand.action(async (uidStr: string) => {
const uid = parseInt(uidStr); const uid = parseInt(uidStr);
if (Number.isNaN(uid)) { if (Number.isNaN(uid)) {
@ -111,7 +122,7 @@ export async function run(
let response = await axios.post("/api/v1/users/remove", { let response = await axios.post("/api/v1/users/remove", {
token: apiKey, token: apiKey,
uid uid,
}); });
if (response.status != 200) { if (response.status != 200) {
@ -125,7 +136,7 @@ export async function run(
return; return;
} }
println("User has been successfully deleted.\n"); println("User has been successfully deleted.\n");
}); });
@ -137,7 +148,7 @@ export async function run(
lookupCommand.option("-e, --email <email>", "Email of User"); lookupCommand.option("-e, --email <email>", "Email of User");
lookupCommand.option("-s, --service", "The user is a service account"); lookupCommand.option("-s, --service", "The user is a service account");
lookupCommand.action(async(options) => { lookupCommand.action(async options => {
// FIXME: redundant parseInt calls // FIXME: redundant parseInt calls
if (options.id) { if (options.id) {
@ -146,7 +157,7 @@ export async function run(
if (Number.isNaN(uid)) { if (Number.isNaN(uid)) {
println("UID (%s) is not a number.\n", uid); println("UID (%s) is not a number.\n", uid);
return; return;
}; }
} }
const response = await axios.post("/api/v1/users/lookup", { const response = await axios.post("/api/v1/users/lookup", {
@ -155,7 +166,7 @@ export async function run(
name: options.name, name: options.name,
username: options.username, username: options.username,
email: options.email, email: options.email,
service: Boolean(options.service) service: Boolean(options.service),
}); });
if (response.status != 200) { if (response.status != 200) {
@ -173,7 +184,11 @@ export async function run(
const { data }: UserLookupSuccess = response.data; const { data }: UserLookupSuccess = response.data;
for (const user of data) { for (const user of data) {
println("UID: %s%s:\n", user.id, (user.isServiceAccount ? " (service)" : "")); println(
"UID: %s%s:\n",
user.id,
user.isServiceAccount ? " (service)" : "",
);
println("- Username: %s\n", user.username); println("- Username: %s\n", user.username);
println("- Name: %s\n", user.name); println("- Name: %s\n", user.name);
println("- Email: %s\n", user.email); println("- Email: %s\n", user.email);
@ -182,12 +197,12 @@ export async function run(
} }
println("%s users found.\n", data.length); println("%s users found.\n", data.length);
}) });
program.addCommand(addCommand); program.addCommand(addCommand);
program.addCommand(removeCommand); program.addCommand(removeCommand);
program.addCommand(lookupCommand); program.addCommand(lookupCommand);
program.parse(argv); program.parse(argv);
await new Promise((resolve) => program.onExit(resolve)); await new Promise(resolve => program.onExit(resolve));
} }

View file

@ -53,22 +53,22 @@ export class SSHCommand extends Command {
recvExitDispatch() { recvExitDispatch() {
this.hasRecievedExitSignal = true; this.hasRecievedExitSignal = true;
this.exitEventHandlers.forEach((eventHandler) => eventHandler()); this.exitEventHandlers.forEach(eventHandler => eventHandler());
let parentElement = this.parent; let parentElement = this.parent;
while (parentElement instanceof SSHCommand) { while (parentElement instanceof SSHCommand) {
parentElement.hasRecievedExitSignal = true; parentElement.hasRecievedExitSignal = true;
parentElement.exitEventHandlers.forEach((eventHandler) => eventHandler()); parentElement.exitEventHandlers.forEach(eventHandler => eventHandler());
parentElement = parentElement.parent; parentElement = parentElement.parent;
}; }
}; }
onExit(callback: (...any: any[]) => void) { onExit(callback: (...any: any[]) => void) {
this.exitEventHandlers.push(callback); this.exitEventHandlers.push(callback);
if (this.hasRecievedExitSignal) callback(); if (this.hasRecievedExitSignal) callback();
}; }
_exit() { _exit() {
this.recvExitDispatch(); this.recvExitDispatch();
@ -81,11 +81,11 @@ export class SSHCommand extends Command {
action(fn: (...args: any[]) => void | Promise<void>): this { action(fn: (...args: any[]) => void | Promise<void>): this {
super.action(fn); super.action(fn);
// @ts-ignore // @ts-expect-error
// prettier-ignore // prettier-ignore
const oldActionHandler: (...args: any[]) => void | Promise<void> = this._actionHandler; const oldActionHandler: (...args: any[]) => void | Promise<void> = this._actionHandler;
// @ts-ignore // @ts-expect-error
this._actionHandler = async (...args: any[]): Promise<void> => { this._actionHandler = async (...args: any[]): Promise<void> => {
if (this.hasRecievedExitSignal) return; if (this.hasRecievedExitSignal) return;
await oldActionHandler(...args); await oldActionHandler(...args);

View file

@ -1,6 +1,8 @@
import type { ServerChannel } from "ssh2"; import type { ServerChannel } from "ssh2";
const pullRate = process.env.KEYBOARD_PULLING_RATE ? parseInt(process.env.KEYBOARD_PULLING_RATE) : 5; const pullRate = process.env.KEYBOARD_PULLING_RATE
? parseInt(process.env.KEYBOARD_PULLING_RATE)
: 5;
const leftEscape = "\x1B[D"; const leftEscape = "\x1B[D";
const rightEscape = "\x1B[C"; const rightEscape = "\x1B[C";
@ -33,19 +35,20 @@ export async function readFromKeyboard(
} else if (character == clientBackspace) { } else if (character == clientBackspace) {
if (line.length == 0) return setTimeout(eventLoop, pullRate); // Here because if we do it in the parent if statement, shit breaks if (line.length == 0) return setTimeout(eventLoop, pullRate); // Here because if we do it in the parent if statement, shit breaks
line = line.substring(0, lineIndex - 1) + line.substring(lineIndex); line = line.substring(0, lineIndex - 1) + line.substring(lineIndex);
if (!disableEcho) { if (!disableEcho) {
const deltaCursor = line.length - lineIndex; const deltaCursor = line.length - lineIndex;
if (deltaCursor == line.length) return setTimeout(eventLoop, pullRate); if (deltaCursor == line.length)
return setTimeout(eventLoop, pullRate);
if (deltaCursor < 0) { if (deltaCursor < 0) {
// Use old technique if the delta is < 0, as the new one is tailored to the start + 1 to end - 1 // Use old technique if the delta is < 0, as the new one is tailored to the start + 1 to end - 1
stream.write(ourBackspace + " " + ourBackspace); stream.write(ourBackspace + " " + ourBackspace);
} else { } else {
// Jump forward to the front, and remove the last character // Jump forward to the front, and remove the last character
stream.write(rightEscape.repeat(deltaCursor) + " " + ourBackspace); stream.write(rightEscape.repeat(deltaCursor) + " " + ourBackspace);
// Go backwards & rerender text & go backwards again (wtf?) // Go backwards & rerender text & go backwards again (wtf?)
stream.write( stream.write(
leftEscape.repeat(deltaCursor + 1) + leftEscape.repeat(deltaCursor + 1) +
@ -53,12 +56,13 @@ export async function readFromKeyboard(
leftEscape.repeat(deltaCursor + 1), leftEscape.repeat(deltaCursor + 1),
); );
} }
lineIndex -= 1; lineIndex -= 1;
} }
} else if (character == "\x1B") { } else if (character == "\x1B") {
if (character == rightEscape) { if (character == rightEscape) {
if (lineIndex + 1 > line.length) return setTimeout(eventLoop, pullRate); if (lineIndex + 1 > line.length)
return setTimeout(eventLoop, pullRate);
lineIndex += 1; lineIndex += 1;
} else if (character == leftEscape) { } else if (character == leftEscape) {
if (lineIndex - 1 < 0) return setTimeout(eventLoop, pullRate); if (lineIndex - 1 < 0) return setTimeout(eventLoop, pullRate);
@ -66,20 +70,20 @@ export async function readFromKeyboard(
} else { } else {
return setTimeout(eventLoop, pullRate); return setTimeout(eventLoop, pullRate);
} }
if (!disableEcho) stream.write(character); if (!disableEcho) stream.write(character);
} else { } else {
lineIndex += 1; lineIndex += 1;
// There isn't a splice method for String prototypes. So, ugh: // There isn't a splice method for String prototypes. So, ugh:
line = line =
line.substring(0, lineIndex - 1) + line.substring(0, lineIndex - 1) +
character + character +
line.substring(lineIndex - 1); line.substring(lineIndex - 1);
if (!disableEcho) { if (!disableEcho) {
let deltaCursor = line.length - lineIndex; let deltaCursor = line.length - lineIndex;
// wtf? // wtf?
if (deltaCursor < 0) { if (deltaCursor < 0) {
console.log( console.log(
@ -87,7 +91,7 @@ export async function readFromKeyboard(
); );
deltaCursor = 0; deltaCursor = 0;
} }
stream.write( stream.write(
line.substring(lineIndex - 1) + leftEscape.repeat(deltaCursor), line.substring(lineIndex - 1) + leftEscape.repeat(deltaCursor),
); );
@ -96,7 +100,7 @@ export async function readFromKeyboard(
} }
setTimeout(eventLoop, pullRate); setTimeout(eventLoop, pullRate);
}; }
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(eventLoop, pullRate); setTimeout(eventLoop, pullRate);