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
|
username: imterah
|
||||||
password: ${{secrets.ACTIONS_PACKAGES_DEPL_KEY}}
|
password: ${{secrets.ACTIONS_PACKAGES_DEPL_KEY}}
|
||||||
|
|
||||||
- name: Build Docker images
|
- name: Build Docker image
|
||||||
run: |
|
run: |
|
||||||
docker build ./backend --tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME
|
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 Docker image
|
||||||
|
|
||||||
- name: Upload all Docker images
|
|
||||||
run: |
|
run: |
|
||||||
docker tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME ghcr.io/imterah/hermes:latest
|
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:$GITHUB_REF_NAME
|
||||||
docker push ghcr.io/imterah/hermes:latest
|
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