chore: Remove all legacy code.

This commit is contained in:
Tera << 8 2024-12-28 15:43:21 -05:00
parent fd4d6bfd65
commit aaacdfd5f4
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
34 changed files with 2 additions and 8317 deletions

View file

@ -32,23 +32,12 @@ jobs:
username: imterah
password: ${{secrets.ACTIONS_PACKAGES_DEPL_KEY}}
- name: Build Docker images
- name: Build Docker image
run: |
docker build ./backend --tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME
docker build -t ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME -f MigrationDockerfile .
docker build ./sshfrontend --tag ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME
- name: Upload all Docker images
- name: Upload Docker image
run: |
docker tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME ghcr.io/imterah/hermes:latest
docker push ghcr.io/imterah/hermes:$GITHUB_REF_NAME
docker push ghcr.io/imterah/hermes:latest
docker tag ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME ghcr.io/imterah/hermes-backend-migration:latest
docker push ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME
docker push ghcr.io/imterah/hermes-backend-migration:latest
docker tag ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME ghcr.io/imterah/hermes-lom:latest
docker push ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME
docker push ghcr.io/imterah/hermes-lom:latest

View file

@ -1,25 +0,0 @@
FROM golang:latest AS build
WORKDIR /build
COPY backend /build
RUN cd api; go build .
FROM node:22.11.0-bookworm AS run
LABEL org.opencontainers.image.source="https://git.terah.dev/imterah/nextnet"
COPY migration-entrypoint.sh /app/entrypoint.sh
COPY backend-legacy/src /app/legacy/src
COPY backend-legacy/prisma /app/legacy/prisma
COPY backend-legacy/tsconfig.json /app/legacy/
COPY backend-legacy/package.json /app/legacy/
COPY backend-legacy/package-lock.json /app/legacy/
WORKDIR /app/legacy
RUN apt update
RUN apt install postgresql -y
RUN npm install --save-dev
RUN npm run build
RUN rm out/**/*.ts out/**/*.map
RUN rm -rf src
RUN npm prune --production
WORKDIR /app/modern
COPY --from=build /build/api/api /app/modern/hermes
RUN echo "{}" >> /app/modern/backends.json
WORKDIR /app
ENTRYPOINT sh entrypoint.sh

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,46 +0,0 @@
#!/usr/bin/env bash
echo "Welcome to the Hermes migration assistant."
if [ ! -f "/tmp/db.json.gz" ]; then
echo "Exporting database contents..."
cd /app/legacy
node out/tools/exportDBContents.js /tmp/db.json.gz
BACKUP_EXIT_CODE=$?
if [ $BACKUP_EXIT_CODE -ne 0 ]; then
echo "Failed to export database contents!"
exit 1
fi
echo "!! IMPORTANT !!"
echo "Database backup contents below:"
echo "==== BEGIN BACKUP ===="
cat /tmp/db.json.gz | base64
echo "==== END BACKUP ===="
echo "When copying, do NOT copy the BEGIN and END sections."
fi
echo "Wiping old database..."
cat >> /tmp/wipe.sql << EOF
CREATE DATABASE temp;
\c temp
DROP DATABASE $HERMES_MIGRATE_POSTGRES_DATABASE;
CREATE DATABASE $HERMES_MIGRATE_POSTGRES_DATABASE;
\c nextnet
DROP DATABASE temp;
EOF
psql "$HERMES_POSTGRES_DSN" < /tmp/wipe.sql
rm -rf /tmp/wipe.sql
echo "Restoring backup..."
cd /app/modern
./hermes -b ./backends.json import --bp /tmp/db.json.gz
echo "Restored backup. If this restore fails after the database has wiped, get a shell into the container,"
echo "copy the backup contents into the container (base64 decoded) at '/tmp/db.json.gz',"
echo "and rerun /app/entrypoint.sh."
echo ""
echo "If further issues continue, open an issue at 'https://git.terah.dev/imterah/hermes/backend'."
echo "If the migration succeeded, congratulations!"
sleep 10000

133
sshfrontend/.gitignore vendored
View file

@ -1,133 +0,0 @@
# Output
out
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View file

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

View file

@ -1,28 +0,0 @@
BSD 3-Clause License
Copyright (c) 2024, Greyson
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,2 +0,0 @@
# NextNet LOM
Lights Out Management, NextNet style

View file

@ -1,8 +0,0 @@
#!/bin/bash
export NODE_ENV="production"
if [[ "$SERVER_BASE_URL" == "" ]]; then
export SERVER_BASE_URL="http://nextnet-api:3000/"
fi
npm start

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,34 +0,0 @@
{
"name": "nextnet-lom",
"version": "1.1.2",
"description": "Lights Out Management, NextNet style",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "cd out && node --enable-source-maps index.js",
"dev": "nodemon --watch src --ext ts,js,mjs,json --exec \"tsc && cd out && node --enable-source-maps index.js\""
},
"keywords": [],
"author": "greysoh",
"license": "BSD-3-Clause",
"devDependencies": {
"@eslint/js": "^9.16.0",
"@types/node": "^22.10.1",
"@types/ssh2": "^1.15.1",
"@types/yargs": "^17.0.33",
"eslint": "^9.16.0",
"globals": "^15.12.0",
"nodemon": "^3.1.7",
"typescript": "^5.7.2",
"typescript-eslint": "^8.16.0"
},
"dependencies": {
"axios": "^1.7.8",
"commander": "^12.1.0",
"patch-package": "^8.0.0",
"ssh2": "^1.16.0",
"string-argv": "^0.3.2"
}
}

View file

@ -1,65 +0,0 @@
import type { Axios } from "axios";
import { run as connection } from "./commands/connections.js";
import { run as backends } from "./commands/backends.js";
import { run as users } from "./commands/users.js";
export type PrintLine = (...str: unknown[]) => void;
export type KeyboardRead = (disableEcho?: boolean) => Promise<string>;
type Command = (
args: string[],
println: PrintLine,
axios: Axios,
apiKey: string,
keyboardRead: KeyboardRead,
) => Promise<void>;
type Commands = {
name: string;
description: string;
run: Command;
}[];
export const commands: Commands = [
{
name: "help",
description: "Prints help",
async run(_args: string[], printf: PrintLine) {
commands.forEach(command => {
printf(`${command.name}: ${command.description}\n`);
});
printf(
"\nRun a command of your choosing with --help to see more options.\n",
);
},
},
{
name: "clear",
description: "Clears screen",
async run(_args: string[], printf: PrintLine) {
printf("\x1B[2J\x1B[3J\x1B[H");
},
},
{
name: "conn",
description: "Manages connections for NextNet",
run: connection,
},
{
name: "user",
description: "Manages users for NextNet",
run: users,
},
{
name: "backend",
description: "Manages backends for NextNet",
run: backends,
},
{
name: "back",
description: "(alias) Manages backends for NextNet",
run: backends,
},
];

View file

@ -1,519 +0,0 @@
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));
}

View file

@ -1,504 +0,0 @@
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));
}

View file

@ -1,215 +0,0 @@
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));
}

View file

@ -1,48 +0,0 @@
import { writeFile } from "node:fs/promises";
import ssh2 from "ssh2";
import { readFromKeyboard } from "./libs/readFromKeyboard.js";
import type { ClientKeys } from "./index.js";
export async function runCopyID(
username: string,
password: string,
keys: ClientKeys,
stream: ssh2.ServerChannel,
) {
stream.write(
"Hey there! I think you're using ssh-copy-id. If this is an error, you may close this terminal.\n",
);
stream.write("Please wait...\n");
const keyData = await readFromKeyboard(stream, true);
stream.write("Parsing key...\n");
const parsedKey = ssh2.utils.parseKey(keyData);
if (parsedKey instanceof Error) {
stream.write(parsedKey.message + "\n");
return stream.close();
}
stream.write("Passed checks. Writing changes...\n");
keys.push({
username,
password,
publicKey: keyData,
});
try {
await writeFile("../keys/clients.json", JSON.stringify(keys, null, 2));
} catch (e) {
console.log(e);
return stream.write(
"ERROR: Failed to save changes! If you're the administrator, view the console for details.\n",
);
}
stream.write("Success!\n");
return stream.close();
}

View file

@ -1,242 +0,0 @@
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { timingSafeEqual } from "node:crypto";
import { format } from "node:util";
import parseArgsStringToArgv from "string-argv";
import baseAxios from "axios";
import ssh2 from "ssh2";
import { readFromKeyboard } from "./libs/readFromKeyboard.js";
import { commands } from "./commands.js";
import { runCopyID } from "./copyID.js";
export type ClientKeys = {
publicKey: string;
username: string;
password: string;
}[];
function checkValue(input: Buffer, allowed: Buffer): boolean {
const autoReject = input.length !== allowed.length;
if (autoReject) allowed = input;
const isMatch = timingSafeEqual(input, allowed);
return !autoReject && isMatch;
}
let serverKeyFile: Buffer | string | undefined;
let clientKeys: ClientKeys = [];
const serverBaseURL: string =
process.env.SERVER_BASE_URL ?? "http://127.0.0.1:3000/";
const axios = baseAxios.create({
baseURL: serverBaseURL,
validateStatus: () => true,
});
try {
clientKeys = JSON.parse(await readFile("../keys/clients.json", "utf8"));
} catch (e) {
console.log("INFO: We don't have the client key file.");
}
try {
serverKeyFile = await readFile("../keys/host.key");
} catch (e) {
console.log(
"ERROR: Failed to read the host key file! Creating new keypair...",
);
await mkdir("../keys").catch(() => null);
const keyPair: { private: string; public: string } = await new Promise(
resolve =>
ssh2.utils.generateKeyPair("ed25519", (err, keyPair) => resolve(keyPair)),
);
await writeFile("../keys/host.key", keyPair.private);
await writeFile("../keys/host.pub", keyPair.public);
serverKeyFile = keyPair.private;
}
if (!serverKeyFile) throw new Error("Somehow failed to fetch the key file!");
const server: ssh2.Server = new ssh2.Server({
hostKeys: [serverKeyFile],
banner: "NextNet-LOM (c) NextNet project et al.",
});
server.on("connection", client => {
let token: string = "";
let username: string = "";
let password: string = "";
client.on("authentication", async auth => {
if (auth.method == "password") {
const response = await axios.post("/api/v1/users/login", {
username: auth.username,
password: auth.password,
});
if (response.status == 403) {
return auth.reject(["password", "publickey"]);
}
token = response.data.token;
username = auth.username;
password = auth.password;
auth.accept();
} else if (auth.method == "publickey") {
const userData = {
username: "",
password: "",
};
for (const rawKey of clientKeys) {
const key = ssh2.utils.parseKey(rawKey.publicKey);
if (key instanceof Error) {
console.log(key);
continue;
}
if (
(rawKey.username == auth.username &&
auth.key.algo == key.type &&
checkValue(auth.key.data, key.getPublicSSH())) ||
(auth.signature &&
key.verify(auth.blob as Buffer, auth.signature, auth.key.algo))
) {
userData.username = rawKey.username;
userData.password = rawKey.password;
}
}
if (!userData.username || !userData.password)
return auth.reject(["password", "publickey"]);
const response = await axios.post("/api/v1/users/login", userData);
if (response.status == 403) {
return auth.reject(["password", "publickey"]);
}
token = response.data.token;
username = userData.username;
password = userData.password;
auth.accept();
} else {
return auth.reject(["password", "publickey"]);
}
});
client.on("ready", () => {
client.on("session", accept => {
const conn = accept();
conn.on("exec", async (accept, reject, info) => {
const stream = accept();
if (
info.command.includes(".ssh/authorized_keys") &&
info.command.startsWith("exec sh -c")
) {
return await runCopyID(username, password, clientKeys, stream);
}
// Matches on ; and &&
const commandsRecv = info.command.split(/;|&&/).map(i => i.trim());
function println(...data: unknown[]) {
stream.write(format(...data).replaceAll("\n", "\r\n"));
}
for (const command of commandsRecv) {
const argv = parseArgsStringToArgv(command);
if (argv[0] == "exit") {
stream.close();
} else {
const command = commands.find(i => i.name == argv[0]);
if (!command) {
stream.write(`Unknown command ${argv[0]}.\r\n`);
continue;
}
await command.run(argv, println, axios, token, disableEcho =>
readFromKeyboard(stream, disableEcho),
);
}
}
return stream.close();
});
// We're dumb. We don't really care.
conn.on("pty", accept => accept());
conn.on("window-change", accept => {
if (typeof accept != "function") return;
accept();
});
conn.on("shell", async accept => {
const stream = accept();
stream.write(
"Welcome to NextNet LOM. Run 'help' to see commands.\r\n\r\n~$ ",
);
function println(...data: unknown[]) {
stream.write(format(...data).replaceAll("\n", "\r\n"));
}
// FIXME (greysoh): wtf? this isn't setting correctly.
// @eslint-disable-next-line
while (true) {
const line = await readFromKeyboard(stream);
stream.write("\r\n");
if (line == "") {
stream.write(`~$ `);
continue;
}
const argv = parseArgsStringToArgv(line);
if (argv[0] == "exit") {
stream.close();
} else {
const command = commands.find(i => i.name == argv[0]);
if (!command) {
stream.write(
`Unknown command ${argv[0]}. Run 'help' to see commands.\r\n~$ `,
);
continue;
}
await command.run(argv, println, axios, token, disableEcho =>
readFromKeyboard(stream, disableEcho),
);
stream.write("~$ ");
}
}
});
});
});
});
server.listen(
2222,
process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1",
);
console.log("Started server at ::2222");

View file

@ -1,108 +0,0 @@
import { Command, type ParseOptions } from "commander";
import { PrintLine } from "../commands";
export class SSHCommand extends Command {
hasRecievedExitSignal: boolean;
println: PrintLine;
exitEventHandlers: ((...any: unknown[]) => void)[];
parent: SSHCommand | null;
/**
* Modified version of the Commander command with slight automated patches, to work with our SSH environment.
* @param println PrintLine function to use
* @param name Optional field for the name of the command
*/
constructor(
println: PrintLine,
name?: string,
disableSSHHelpPatching: boolean = false,
) {
super(name);
this.exitEventHandlers = [];
this.configureOutput({
writeOut: str => println(str),
writeErr: str => {
if (this.hasRecievedExitSignal) return;
println(str);
},
});
if (!disableSSHHelpPatching) {
const sshCommand = new SSHCommand(println, "help", true);
sshCommand.description("display help for command");
sshCommand.argument("[command]", "command to show help for");
sshCommand.action(() => {
this.hasRecievedExitSignal = true;
if (process.env.NODE_ENV != "production") {
println(
"Caught irrecoverable crash (command help call) in patchCommander\n",
);
} else {
println("Aborted\n");
}
});
this.addCommand(sshCommand);
}
}
recvExitDispatch() {
this.hasRecievedExitSignal = true;
this.exitEventHandlers.forEach(eventHandler => eventHandler());
let parentElement = this.parent;
while (parentElement instanceof SSHCommand) {
parentElement.hasRecievedExitSignal = true;
parentElement.exitEventHandlers.forEach(eventHandler => eventHandler());
parentElement = parentElement.parent;
}
}
onExit(callback: (...any: any[]) => void) {
this.exitEventHandlers.push(callback);
if (this.hasRecievedExitSignal) callback();
}
_exit() {
this.recvExitDispatch();
}
_exitCallback() {
this.recvExitDispatch();
}
action(fn: (...args: any[]) => void | Promise<void>): this {
super.action(fn);
// @ts-expect-error: This parameter is private, but we need control over it.
// prettier-ignore
const oldActionHandler: (...args: any[]) => void | Promise<void> = this._actionHandler;
// @ts-expect-error: Overriding private parameters (but this works)
this._actionHandler = async (...args: any[]): Promise<void> => {
if (this.hasRecievedExitSignal) return;
await oldActionHandler(...args);
this.recvExitDispatch();
};
return this;
}
parse(argv?: readonly string[], options?: ParseOptions): this {
super.parse(["nextruntime", ...(argv ?? [])], options);
return this;
}
createCommand(name: string) {
const command = new SSHCommand(this.println, name);
return command;
}
}

View file

@ -1,109 +0,0 @@
import type { ServerChannel } from "ssh2";
const pullRate = process.env.KEYBOARD_PULLING_RATE
? parseInt(process.env.KEYBOARD_PULLING_RATE)
: 5;
const leftEscape = "\x1B[D";
const rightEscape = "\x1B[C";
const ourBackspace = "\u0008";
const clientBackspace = "\x7F";
export async function readFromKeyboard(
stream: ServerChannel,
disableEcho: boolean = false,
): Promise<string> {
let promise: (value: string | PromiseLike<string>) => void;
let line = "";
let lineIndex = 0;
async function eventLoop(): Promise<any> {
const readStreamDataBuf = stream.read();
if (readStreamDataBuf == null) return setTimeout(eventLoop, pullRate);
const readStreamData = readStreamDataBuf.toString();
// Fixes several bugs (incl. potential social eng. exploits, ssh-copy-id being broken, etc)
for (const character of readStreamData.split("")) {
if (character == "\x03") {
stream.write("^C");
return promise("");
} else if (character == "\r" || character == "\n") {
return promise(line.replace("\r", ""));
} 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
line = line.substring(0, lineIndex - 1) + line.substring(lineIndex);
if (!disableEcho) {
const deltaCursor = line.length - lineIndex;
if (deltaCursor == line.length)
return setTimeout(eventLoop, pullRate);
if (deltaCursor < 0) {
// 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);
} else {
// Jump forward to the front, and remove the last character
stream.write(rightEscape.repeat(deltaCursor) + " " + ourBackspace);
// Go backwards & rerender text & go backwards again (wtf?)
stream.write(
leftEscape.repeat(deltaCursor + 1) +
line.substring(lineIndex - 1) +
leftEscape.repeat(deltaCursor + 1),
);
}
lineIndex -= 1;
}
} else if (character == "\x1B") {
if (character == rightEscape) {
if (lineIndex + 1 > line.length)
return setTimeout(eventLoop, pullRate);
lineIndex += 1;
} else if (character == leftEscape) {
if (lineIndex - 1 < 0) return setTimeout(eventLoop, pullRate);
lineIndex -= 1;
} else {
return setTimeout(eventLoop, pullRate);
}
if (!disableEcho) stream.write(character);
} else {
lineIndex += 1;
// There isn't a splice method for String prototypes. So, ugh:
line =
line.substring(0, lineIndex - 1) +
character +
line.substring(lineIndex - 1);
if (!disableEcho) {
let deltaCursor = line.length - lineIndex;
// wtf?
if (deltaCursor < 0) {
console.log(
"FIXME: somehow, our deltaCursor value is negative! please investigate me",
);
deltaCursor = 0;
}
stream.write(
line.substring(lineIndex - 1) + leftEscape.repeat(deltaCursor),
);
}
}
}
setTimeout(eventLoop, pullRate);
}
return new Promise(resolve => {
setTimeout(eventLoop, pullRate);
promise = resolve;
});
}

View file

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