chore: Remove all legacy code.
This commit is contained in:
parent
fd4d6bfd65
commit
aaacdfd5f4
34 changed files with 2 additions and 8317 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
];
|
3302
backend-legacy/package-lock.json
generated
3302
backend-legacy/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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";
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "username" TEXT;
|
|
@ -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"
|
|
@ -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[]
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
});
|
|
@ -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"]
|
||||
}
|
|
@ -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
133
sshfrontend/.gitignore
vendored
|
@ -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.*
|
|
@ -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
|
|
@ -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.
|
|
@ -1,2 +0,0 @@
|
|||
# NextNet LOM
|
||||
Lights Out Management, NextNet style
|
|
@ -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
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
];
|
2564
sshfrontend/package-lock.json
generated
2564
sshfrontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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));
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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");
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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"]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue