chore: Restructure files.
This commit is contained in:
parent
559588f726
commit
d25da9091e
93 changed files with 38 additions and 26 deletions
519
sshfrontend/src/commands/backends.ts
Normal file
519
sshfrontend/src/commands/backends.ts
Normal file
|
@ -0,0 +1,519 @@
|
|||
import type { Axios } from "axios";
|
||||
|
||||
import { SSHCommand } from "../libs/patchCommander.js";
|
||||
import type { PrintLine, KeyboardRead } from "../commands.js";
|
||||
|
||||
type BackendLookupSuccess = {
|
||||
success: boolean;
|
||||
data: {
|
||||
id: number;
|
||||
|
||||
name: string;
|
||||
description: string;
|
||||
backend: string;
|
||||
connectionDetails?: string;
|
||||
logs: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
const addRequiredOptions = {
|
||||
ssh: ["sshKey", "username", "host"],
|
||||
|
||||
passyfire: ["host"],
|
||||
};
|
||||
|
||||
export async function run(
|
||||
argv: string[],
|
||||
println: PrintLine,
|
||||
axios: Axios,
|
||||
token: string,
|
||||
readKeyboard: KeyboardRead,
|
||||
) {
|
||||
const program = new SSHCommand(println);
|
||||
program.description("Manages backends for NextNet");
|
||||
program.version("v1.0.0");
|
||||
|
||||
const addBackend = new SSHCommand(println, "add");
|
||||
|
||||
addBackend.description("Adds a backend");
|
||||
addBackend.argument("<name>", "Name of the backend");
|
||||
|
||||
addBackend.argument(
|
||||
"<provider>",
|
||||
"Provider of the backend (ex. passyfire, ssh)",
|
||||
);
|
||||
|
||||
addBackend.option(
|
||||
"-d, --description <description>",
|
||||
"Description for the backend",
|
||||
);
|
||||
|
||||
addBackend.option(
|
||||
"-f, --force-custom-parameters",
|
||||
"If turned on, this forces you to use custom parameters",
|
||||
);
|
||||
|
||||
addBackend.option(
|
||||
"-c, --custom-parameters <parameters>",
|
||||
"Custom parameters. Use this if the backend you're using isn't native to SSH yet, or if you manually turn on -f.",
|
||||
);
|
||||
|
||||
// SSH provider
|
||||
addBackend.option(
|
||||
"-k, --ssh-key <private-key>",
|
||||
"(SSH) SSH private key to use to authenticate with the server",
|
||||
);
|
||||
|
||||
addBackend.option(
|
||||
"-u, --username <user>",
|
||||
"(SSH, PassyFire) Username to authenticate with. With PassyFire, it's the username you create",
|
||||
);
|
||||
|
||||
addBackend.option(
|
||||
"-h, --host <host>",
|
||||
"(SSH, PassyFire) Host to connect to. With PassyFire, it's what you listen on",
|
||||
);
|
||||
|
||||
// PassyFire provider
|
||||
addBackend.option(
|
||||
"-pe, --is-proxied",
|
||||
"(PassyFire) Specify if you're behind a proxy or not so we can get the right IP",
|
||||
);
|
||||
|
||||
addBackend.option(
|
||||
"-pp, --proxied-port <port>",
|
||||
"(PassyFire) If you're behind a proxy, and the port is different, specify the port to return",
|
||||
);
|
||||
|
||||
addBackend.option("-g, --guest", "(PassyFire) Enable the guest user");
|
||||
|
||||
addBackend.option(
|
||||
"-ua, --user-ask",
|
||||
"(PassyFire) Ask what users you want to create",
|
||||
);
|
||||
|
||||
addBackend.option(
|
||||
"-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;
|
||||
proxiedPort?: string;
|
||||
guest?: boolean;
|
||||
userAsk?: boolean;
|
||||
password?: string;
|
||||
},
|
||||
) => {
|
||||
// @ts-expect-error: Yes it can index for what we need it to do.
|
||||
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"]) {
|
||||
// @ts-expect-error: No.
|
||||
const hasArgument = options[argument];
|
||||
|
||||
if (!hasArgument) {
|
||||
return println("ERROR: Missing argument '%s'\n", argument);
|
||||
}
|
||||
}
|
||||
|
||||
const unstringifiedArguments: {
|
||||
ip?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
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"]) {
|
||||
// @ts-expect-error: No.
|
||||
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({
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.userAsk) {
|
||||
let shouldContinueAsking: boolean = true;
|
||||
|
||||
while (shouldContinueAsking) {
|
||||
println("Creating a user.\nUsername: ");
|
||||
const username = await readKeyboard();
|
||||
|
||||
let passwordConfirmOne = "a";
|
||||
let passwordConfirmTwo = "b";
|
||||
|
||||
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) ");
|
||||
shouldContinueAsking = (await readKeyboard())
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.startsWith("y");
|
||||
|
||||
println("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
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);
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/backends/create", {
|
||||
token,
|
||||
|
||||
name,
|
||||
description: options.description,
|
||||
backend: provider,
|
||||
|
||||
connectionDetails,
|
||||
});
|
||||
|
||||
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 backend!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
println("Successfully created the backend.\n");
|
||||
},
|
||||
);
|
||||
|
||||
const removeBackend = new SSHCommand(println, "rm");
|
||||
removeBackend.description("Removes a backend");
|
||||
removeBackend.argument("<id>", "ID of the backend");
|
||||
|
||||
removeBackend.action(async (idStr: string) => {
|
||||
const id: number = parseInt(idStr);
|
||||
|
||||
if (Number.isNaN(id)) {
|
||||
println("ID (%s) is not a number.\n", idStr);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/backends/remove", {
|
||||
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 deleting backend!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
println("Backend has been successfully deleted.\n");
|
||||
});
|
||||
|
||||
const lookupBackend = new SSHCommand(println, "find");
|
||||
lookupBackend.description("Looks up a backend based on your arguments");
|
||||
|
||||
lookupBackend.option("-n, --name <name>", "Name of the backend");
|
||||
|
||||
lookupBackend.option(
|
||||
"-p, --provider <provider>",
|
||||
"Provider of the backend (ex. passyfire, ssh)",
|
||||
);
|
||||
|
||||
lookupBackend.option(
|
||||
"-d, --description <description>",
|
||||
"Description for the backend",
|
||||
);
|
||||
|
||||
lookupBackend.option(
|
||||
"-e, --parse-connection-details",
|
||||
"If specified, we automatically parse the connection details to make them human readable, if standard JSON.",
|
||||
);
|
||||
|
||||
lookupBackend.action(
|
||||
async (options: {
|
||||
name?: string;
|
||||
provider?: string;
|
||||
description?: string;
|
||||
parseConnectionDetails?: boolean;
|
||||
}) => {
|
||||
const response = await axios.post("/api/v1/backends/lookup", {
|
||||
token,
|
||||
|
||||
name: options.name,
|
||||
description: options.description,
|
||||
|
||||
backend: options.provider,
|
||||
});
|
||||
|
||||
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 looking up backends!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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 :(
|
||||
|
||||
// eslint-disable-next-line
|
||||
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");
|
||||
logsCommand.description("View logs for a backend");
|
||||
logsCommand.argument("<id>", "ID of the backend");
|
||||
|
||||
logsCommand.action(async (idStr: string) => {
|
||||
const id: number = parseInt(idStr);
|
||||
|
||||
if (Number.isNaN(id)) {
|
||||
println("ID (%s) is not a number.\n", idStr);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/backends/lookup", {
|
||||
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 getting logs!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { data }: BackendLookupSuccess = response.data;
|
||||
const ourBackend = data.find(i => i.id == id);
|
||||
|
||||
if (!ourBackend) return println("Could not find the backend!\n");
|
||||
ourBackend.logs.forEach(log => println("%s\n", log));
|
||||
});
|
||||
|
||||
program.addCommand(addBackend);
|
||||
program.addCommand(removeBackend);
|
||||
program.addCommand(lookupBackend);
|
||||
program.addCommand(logsCommand);
|
||||
|
||||
program.parse(argv);
|
||||
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise(resolve => program.onExit(resolve));
|
||||
}
|
504
sshfrontend/src/commands/connections.ts
Normal file
504
sshfrontend/src/commands/connections.ts
Normal file
|
@ -0,0 +1,504 @@
|
|||
import type { Axios } from "axios";
|
||||
|
||||
import { SSHCommand } from "../libs/patchCommander.js";
|
||||
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
|
||||
function difference(a: any[], b: any[]) {
|
||||
return a.filter(x => b.indexOf(x) < 0);
|
||||
}
|
||||
|
||||
type InboundConnectionSuccess = {
|
||||
success: true;
|
||||
data: {
|
||||
ip: string;
|
||||
port: number;
|
||||
|
||||
connectionDetails: {
|
||||
sourceIP: string;
|
||||
sourcePort: number;
|
||||
destPort: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
type LookupCommandSuccess = {
|
||||
success: true;
|
||||
data: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
sourceIP: string;
|
||||
sourcePort: number;
|
||||
destPort: number;
|
||||
providerID: number;
|
||||
autoStart: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
export async function run(
|
||||
argv: string[],
|
||||
println: PrintLine,
|
||||
axios: Axios,
|
||||
token: string,
|
||||
) {
|
||||
if (argv.length == 1)
|
||||
return println(
|
||||
"error: no arguments specified! run %s --help to see commands.\n",
|
||||
argv[0],
|
||||
);
|
||||
|
||||
const program = new SSHCommand(println);
|
||||
program.description("Manages connections for NextNet");
|
||||
program.version("v1.0.0");
|
||||
|
||||
const addCommand = new SSHCommand(println, "add");
|
||||
addCommand.description("Creates a new connection");
|
||||
|
||||
addCommand.argument(
|
||||
"<backend_id>",
|
||||
"The backend ID to use. Can be fetched by the command 'backend search'",
|
||||
);
|
||||
|
||||
addCommand.argument("<name>", "The name for the tunnel");
|
||||
addCommand.argument("<protocol>", "The protocol to use. Either TCP or UDP");
|
||||
|
||||
addCommand.argument(
|
||||
"<source>",
|
||||
"Source IP and port combo (ex. '192.168.0.63:25565'",
|
||||
);
|
||||
|
||||
addCommand.argument("<dest_port>", "Destination port to use");
|
||||
addCommand.option("-d, --description", "Description for the tunnel");
|
||||
|
||||
addCommand.action(
|
||||
async (
|
||||
providerIDStr: string,
|
||||
name: string,
|
||||
protocolRaw: string,
|
||||
source: string,
|
||||
destPortRaw: string,
|
||||
options: {
|
||||
description?: string;
|
||||
},
|
||||
) => {
|
||||
const providerID = parseInt(providerIDStr);
|
||||
|
||||
if (Number.isNaN(providerID)) {
|
||||
println("ID (%s) is not a number\n", providerIDStr);
|
||||
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;
|
||||
}
|
||||
|
||||
println("Successfully created connection.\n");
|
||||
},
|
||||
);
|
||||
|
||||
const lookupCommand = new SSHCommand(println, "find");
|
||||
|
||||
lookupCommand.description(
|
||||
"Looks up all connections based on the arguments you specify",
|
||||
);
|
||||
|
||||
lookupCommand.option(
|
||||
"-b, --backend-id <id>",
|
||||
"The backend ID to use. Can be fetched by 'back find'",
|
||||
);
|
||||
|
||||
lookupCommand.option("-n, --name <name>", "The name for the tunnel");
|
||||
|
||||
lookupCommand.option(
|
||||
"-p, --protocol <protocol>",
|
||||
"The protocol to use. Either TCP or UDP",
|
||||
);
|
||||
|
||||
lookupCommand.option(
|
||||
"-s <source>, --source",
|
||||
"Source IP and port combo (ex. '192.168.0.63:25565'",
|
||||
);
|
||||
|
||||
lookupCommand.option("-d, --dest-port <port>", "Destination port to use");
|
||||
|
||||
lookupCommand.option(
|
||||
"-o, --description <description>",
|
||||
"Description for the tunnel",
|
||||
);
|
||||
|
||||
lookupCommand.action(
|
||||
async (options: {
|
||||
backendId?: string;
|
||||
destPort?: string;
|
||||
name?: string;
|
||||
protocol?: string;
|
||||
source?: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
let numberBackendID: number | undefined;
|
||||
|
||||
let sourceIP: string | undefined;
|
||||
let sourcePort: number | undefined;
|
||||
|
||||
let destPort: number | undefined;
|
||||
|
||||
if (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");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
startTunnel.description("Starts a tunnel");
|
||||
startTunnel.argument("<id>", "Tunnel ID to start");
|
||||
|
||||
startTunnel.action(async (idStr: string) => {
|
||||
const id = parseInt(idStr);
|
||||
|
||||
if (Number.isNaN(id)) {
|
||||
println("ID (%s) is not a number\n", idStr);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/forward/start", {
|
||||
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 starting the connection!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
println("Successfully started tunnel.\n");
|
||||
return;
|
||||
});
|
||||
|
||||
const stopTunnel = new SSHCommand(println, "stop");
|
||||
stopTunnel.description("Stops a tunnel");
|
||||
stopTunnel.argument("<id>", "Tunnel ID to stop");
|
||||
|
||||
stopTunnel.action(async (idStr: string) => {
|
||||
const id = parseInt(idStr);
|
||||
|
||||
if (Number.isNaN(id)) {
|
||||
println("ID (%s) is not a number\n", idStr);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/forward/stop", {
|
||||
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 stopping a connection!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
println("Successfully stopped tunnel.\n");
|
||||
});
|
||||
|
||||
const getInbound = new SSHCommand(println, "get-inbound");
|
||||
getInbound.description("Shows all current connections");
|
||||
getInbound.argument("<id>", "Tunnel ID to view inbound connections of");
|
||||
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.action(
|
||||
async (
|
||||
idStr: string,
|
||||
options: {
|
||||
tail?: boolean;
|
||||
tailPullRate?: string;
|
||||
},
|
||||
): Promise<void> => {
|
||||
const pullRate: number = options.tailPullRate
|
||||
? parseInt(options.tailPullRate)
|
||||
: 2000;
|
||||
const id = parseInt(idStr);
|
||||
|
||||
if (Number.isNaN(id)) {
|
||||
println("ID (%s) is not a number\n", idStr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isNaN(pullRate)) {
|
||||
println("Pull rate is not a number\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.tail) {
|
||||
let previousEntries: string[] = [];
|
||||
|
||||
// FIXME?
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
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", {
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
removeTunnel.description("Removes a tunnel");
|
||||
removeTunnel.argument("<id>", "Tunnel ID to remove");
|
||||
|
||||
removeTunnel.action(async (idStr: string) => {
|
||||
const id = parseInt(idStr);
|
||||
|
||||
if (Number.isNaN(id)) {
|
||||
println("ID (%s) is not a number\n", idStr);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/forward/remove", {
|
||||
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 deleting connection!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
println("Successfully deleted connection.\n");
|
||||
});
|
||||
|
||||
program.addCommand(addCommand);
|
||||
program.addCommand(lookupCommand);
|
||||
program.addCommand(startTunnel);
|
||||
program.addCommand(stopTunnel);
|
||||
program.addCommand(getInbound);
|
||||
program.addCommand(removeTunnel);
|
||||
|
||||
program.parse(argv);
|
||||
await new Promise(resolve => program.onExit(resolve));
|
||||
}
|
215
sshfrontend/src/commands/users.ts
Normal file
215
sshfrontend/src/commands/users.ts
Normal file
|
@ -0,0 +1,215 @@
|
|||
import type { Axios } from "axios";
|
||||
|
||||
import { SSHCommand } from "../libs/patchCommander.js";
|
||||
import type { PrintLine, KeyboardRead } from "../commands.js";
|
||||
|
||||
type UserLookupSuccess = {
|
||||
success: true;
|
||||
data: {
|
||||
id: number;
|
||||
isServiceAccount: boolean;
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export async function run(
|
||||
argv: string[],
|
||||
println: PrintLine,
|
||||
axios: Axios,
|
||||
apiKey: string,
|
||||
readKeyboard: KeyboardRead,
|
||||
) {
|
||||
if (argv.length == 1)
|
||||
return println(
|
||||
"error: no arguments specified! run %s --help to see commands.\n",
|
||||
argv[0],
|
||||
);
|
||||
|
||||
const program = new SSHCommand(println);
|
||||
program.description("Manages users for NextNet");
|
||||
program.version("v1.0.0");
|
||||
|
||||
const addCommand = new SSHCommand(println, "add");
|
||||
addCommand.description("Create a new user");
|
||||
addCommand.argument("<username>", "Username of new user");
|
||||
addCommand.argument("<email>", "Email of new user");
|
||||
addCommand.argument("[name]", "Name of new user (defaults to username)");
|
||||
|
||||
addCommand.option("-p, --password", "Password of User");
|
||||
addCommand.option(
|
||||
"-a, --ask-password, --ask-pass, --askpass",
|
||||
"Asks for a password. Hides output",
|
||||
);
|
||||
|
||||
addCommand.option(
|
||||
"-s, --service-account, --service",
|
||||
"Turns the user into a service account",
|
||||
);
|
||||
|
||||
addCommand.action(
|
||||
async (
|
||||
username: string,
|
||||
email: string,
|
||||
name: string,
|
||||
options: {
|
||||
password?: string;
|
||||
askPassword?: boolean;
|
||||
isServiceAccount?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (!options.password && !options.askPassword) {
|
||||
println("No password supplied, and askpass has not been supplied.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
let password: string = "";
|
||||
|
||||
if (options.askPassword) {
|
||||
let passwordConfirmOne = "a";
|
||||
let passwordConfirmTwo = "b";
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
password = passwordConfirmOne;
|
||||
} else {
|
||||
// @ts-expect-error: From the first check we do, we know this is safe (you MUST specify a password)
|
||||
password = options.password;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/users/create", {
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
|
||||
allowUnsafeGlobalTokens: options.isServiceAccount,
|
||||
});
|
||||
|
||||
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.\nToken: %s\n", response.data.token);
|
||||
},
|
||||
);
|
||||
|
||||
const removeCommand = new SSHCommand(println, "rm");
|
||||
removeCommand.description("Remove a user");
|
||||
removeCommand.argument("<uid>", "ID of user to remove");
|
||||
|
||||
removeCommand.action(async (uidStr: string) => {
|
||||
const uid = parseInt(uidStr);
|
||||
|
||||
if (Number.isNaN(uid)) {
|
||||
println("UID (%s) is not a number.\n", uid);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/users/remove", {
|
||||
token: apiKey,
|
||||
uid,
|
||||
});
|
||||
|
||||
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 deleting user!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
println("User has been successfully deleted.\n");
|
||||
});
|
||||
|
||||
const lookupCommand = new SSHCommand(println, "find");
|
||||
lookupCommand.description("Find a user");
|
||||
lookupCommand.option("-i, --id <id>", "UID of User");
|
||||
lookupCommand.option("-n, --name <name>", "Name of User");
|
||||
lookupCommand.option("-u, --username <username>", "Username of User");
|
||||
lookupCommand.option("-e, --email <email>", "Email of User");
|
||||
lookupCommand.option("-s, --service", "The user is a service account");
|
||||
|
||||
lookupCommand.action(async options => {
|
||||
// FIXME: redundant parseInt calls
|
||||
|
||||
if (options.id) {
|
||||
const uid = parseInt(options.id);
|
||||
|
||||
if (Number.isNaN(uid)) {
|
||||
println("UID (%s) is not a number.\n", uid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/users/lookup", {
|
||||
token: apiKey,
|
||||
id: options.id ? parseInt(options.id) : undefined,
|
||||
name: options.name,
|
||||
username: options.username,
|
||||
email: options.email,
|
||||
service: Boolean(options.service),
|
||||
});
|
||||
|
||||
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 finding users!\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { data }: UserLookupSuccess = response.data;
|
||||
|
||||
for (const user of data) {
|
||||
println(
|
||||
"UID: %s%s:\n",
|
||||
user.id,
|
||||
user.isServiceAccount ? " (service)" : "",
|
||||
);
|
||||
println("- Username: %s\n", user.username);
|
||||
println("- Name: %s\n", user.name);
|
||||
println("- Email: %s\n", user.email);
|
||||
|
||||
println("\n");
|
||||
}
|
||||
|
||||
println("%s users found.\n", data.length);
|
||||
});
|
||||
|
||||
program.addCommand(addCommand);
|
||||
program.addCommand(removeCommand);
|
||||
program.addCommand(lookupCommand);
|
||||
|
||||
program.parse(argv);
|
||||
await new Promise(resolve => program.onExit(resolve));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue