Web interface #525
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
|
# System
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# npm
|
# npm
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
cookieSecret: 822d8c0f-85ee-4bb9-a3c6-7e4c7252fc81
|
|
||||||
mongo:
|
|
||||||
uri: mongodb://127.0.0.1
|
|
||||||
databaseName: bdsWeb
|
|
||||||
sshServer:
|
|
||||||
hostKeys:
|
|
||||||
- |
|
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
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
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
port: 8022
|
|
189
packages/web/src/auth.ts
Normal file
189
packages/web/src/auth.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import session from "express-session";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import { localConfig } from "./config.js";
|
||||||
|
import { mongoDatabase } from "./databaseConnect.js";
|
||||||
|
|
||||||
|
declare module "express-session" {
|
||||||
|
interface SessionData {
|
||||||
|
userID: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieCollection = mongoDatabase.collection<{sid: string, session: session.SessionData}>("cookies");
|
||||||
|
class SessionMongo extends session.Store {
|
||||||
|
/* temporary storage session on sync to Database */
|
||||||
|
readonly tmpSession = new Map<string, session.SessionData>();
|
||||||
|
|
||||||
|
async destroy(sid: string, callback?: (err?: any) => void) {
|
||||||
|
try {
|
||||||
|
cookieCollection.findOneAndDelete({sid});
|
||||||
|
if (this.tmpSession.has(sid)) this.tmpSession.delete(sid);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(sid: string, session: session.SessionData, callback?: (err?: any) => void) {
|
||||||
|
if (this.tmpSession.has(sid)) return callback();
|
||||||
|
this.tmpSession.set(sid, session);
|
||||||
|
session = typeof session["toJSON"] === "function" ? session["toJSON"]() : session;
|
||||||
|
try {
|
||||||
|
if (await cookieCollection.findOne({sid})) await cookieCollection.findOneAndUpdate({sid}, {session});
|
||||||
|
else await cookieCollection.insertOne({sid, session});
|
||||||
|
this.tmpSession.delete(sid);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(sid: string, callback: (err?: any, session?: session.SessionData) => void) {
|
||||||
|
if (this.tmpSession.has(sid)) return callback(null, this.tmpSession.get(sid));
|
||||||
|
try {
|
||||||
|
await cookieCollection.findOne({sid}).then(res => !res ? callback() : callback(null, res.session));
|
||||||
|
} catch (err) {
|
||||||
|
callback(err, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(callback?: (err?: any) => void) {
|
||||||
|
try {
|
||||||
|
await cookieCollection.deleteMany({});
|
||||||
|
this.tmpSession.clear();
|
||||||
|
callback();
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async all(callback: (err?: any, obj?: session.SessionData[] | { [sid: string]: session.SessionData; }) => void) {
|
||||||
|
try {
|
||||||
|
await cookieCollection.find({}).toArray().then(cookies => callback(null, cookies.reduce((acc, cookie) => {acc[cookie.sid] = cookie.session; return acc;}, {})), err => callback(err));
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cookie = session({
|
||||||
|
secret: localConfig.cookieSecret,
|
||||||
|
name: "bdsAuth",
|
||||||
|
saveUninitialized: true,
|
||||||
|
resave: true,
|
||||||
|
unset: "destroy",
|
||||||
|
cookie: {
|
||||||
|
httpOnly: false,
|
||||||
|
secure: false,
|
||||||
|
signed: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 30 * 2,
|
||||||
|
},
|
||||||
|
store: new SessionMongo(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function passwordEncrypt(input: string): Promise<{hash: string, salt: string}> {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const secret = crypto.randomBytes(24);
|
||||||
|
return new Promise((done, reject) => {
|
||||||
|
crypto.scrypt(secret, "salt", 24, (err, key) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
const cipher = crypto.createCipheriv("aes-192-cbc", key, iv);
|
||||||
|
cipher.on("error", reject);
|
||||||
|
return done({
|
||||||
|
hash: Buffer.from(cipher.update(input, "utf8", "hex") + cipher.final("hex"), "utf8").toString("base64"),
|
||||||
|
salt: Buffer.from(iv.toString("hex") + "::::" + secret.toString("hex"), "utf8").toString("base64")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function passwordDecrypt(hash: string, salt: string): Promise<string> {
|
||||||
|
const hashSplit = Buffer.from(salt, "base64").toString("utf8").split("::::");
|
||||||
|
return new Promise((done, reject) => {
|
||||||
|
const iv = Buffer.from(hashSplit.at(0), "hex");
|
||||||
|
const secret = Buffer.from(hashSplit.at(1), "hex");
|
||||||
|
crypto.scrypt(secret, "salt", 24, (err, key) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
const decipher = crypto.createDecipheriv("aes-192-cbc", key, iv);
|
||||||
|
decipher.on("error", reject);
|
||||||
|
return done(decipher.update(Buffer.from(hash, "base64").toString(), "hex", "utf8") + decipher.final("utf8"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSSHKey(): Promise<{privateKey: string, publicKey: string}> {
|
||||||
|
return new Promise((done, reject) => {
|
||||||
|
crypto.generateKeyPair("rsa", {
|
||||||
|
modulusLength: 3072,
|
||||||
|
privateKeyEncoding: {
|
||||||
|
format: "pem",
|
||||||
|
type: "pkcs1"
|
||||||
|
},
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: "pkcs1",
|
||||||
|
format: "pem"
|
||||||
|
}
|
||||||
|
}, (err, publicKey: string, privateKey: string) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
done({
|
||||||
|
privateKey,
|
||||||
|
publicKey
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type userSorage = {
|
||||||
|
/** Unique user ID */
|
||||||
|
readonly userID: string;
|
||||||
|
|
||||||
|
/** User create date */
|
||||||
|
readonly createAt: Date;
|
||||||
|
|
||||||
|
/** Email to auth */
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
/** Auth password */
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
/** API Token auth */
|
||||||
|
tokens: string[];
|
||||||
|
|
||||||
|
/** Minecraft username to maneger access list */
|
||||||
|
mcUsername: {
|
||||||
|
Bedrock: string;
|
||||||
|
Java: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const usersCollection = mongoDatabase.collection<userSorage>("usersAuth");
|
||||||
|
|
||||||
|
export const random = () => {
|
||||||
|
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
||||||
|
return ([
|
||||||
|
crypto.pseudoRandomBytes(8).toString("hex"),
|
||||||
|
crypto.pseudoRandomBytes(4).toString("hex"),
|
||||||
|
crypto.pseudoRandomBytes(4).toString("hex"),
|
||||||
|
crypto.pseudoRandomBytes(4).toString("hex"),
|
||||||
|
crypto.pseudoRandomBytes(12).toString("hex"),
|
||||||
|
]).join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateUserID() {
|
||||||
|
let userID: string;
|
||||||
|
while (true) if (!(await usersCollection.findOne({userID: (userID = random())}))) break;
|
||||||
|
return userID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateToken() {
|
||||||
|
const genToken = () => {
|
||||||
|
let data = Array(crypto.randomInt(3, 8)+1).fill(null).map(() => crypto.pseudoRandomBytes(crypto.randomInt(1, 6)).toString("hex"));
|
||||||
|
let scg = "tk_";
|
||||||
|
|
||||||
|
scg += data.shift() + data.pop();
|
||||||
|
scg += "0" + data.join("-")
|
||||||
|
|
||||||
|
return scg;
|
||||||
|
};
|
||||||
|
|
||||||
|
let token: string;
|
||||||
|
while (true) if (!(await usersCollection.findOne({tokens: [(token = genToken())]}))) break;
|
||||||
|
return token;
|
||||||
|
}
|
@ -1,58 +1,33 @@
|
|||||||
import extendFs from "@sirherobrine23/extends";
|
import { randomBytes } from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import yaml from "yaml";
|
import { createSSHKey } from "./auth.js";
|
||||||
import { createSSHKey } from "./db.js";
|
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
|
|
||||||
export type configSchema = {
|
export type configFile = {
|
||||||
port: number;
|
/** HTTP port listen */
|
||||||
|
portListen: number;
|
||||||
|
domain?: string;
|
||||||
|
/** Super cookie secret */
|
||||||
cookieSecret: string;
|
cookieSecret: string;
|
||||||
sshServer: {
|
/** MongoDB URI connection */
|
||||||
|
mongoConnection: string;
|
||||||
|
mongoDatabase?: string;
|
||||||
|
/** SSH Server config */
|
||||||
|
ssh?: {
|
||||||
|
port: number;
|
||||||
hostKeys: string[];
|
hostKeys: string[];
|
||||||
port?: number;
|
|
||||||
banner?: string;
|
|
||||||
};
|
};
|
||||||
mongo?: {
|
|
||||||
uri: string;
|
|
||||||
databaseName?: string;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getConfig() {
|
const sshKeys = Object.keys(process.env).filter(name => name.startsWith("SSH_HOST")).map(name => path.resolve(process.cwd(), process.env[name]));
|
||||||
const configPath = path.resolve(process.cwd(), process.env.CONFIG_PATH || "./config.yml");
|
|
||||||
let userConfig: configSchema|undefined;
|
|
||||||
const config: configSchema = {
|
|
||||||
port: Number(process.env.PORT || "3000"),
|
|
||||||
cookieSecret: process.env.COOKIE_SECRET || randomUUID(),
|
|
||||||
mongo: {
|
|
||||||
uri: process.env.MONGO_URI || "mongodb://127.0.0.1",
|
|
||||||
databaseName: process.env.DB_NAME || "bdsWeb"
|
|
||||||
},
|
|
||||||
sshServer: {
|
|
||||||
hostKeys: [
|
|
||||||
(await createSSHKey()).private,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (await extendFs.exists(configPath)) {
|
export const localConfig: configFile = {
|
||||||
userConfig = yaml.parse(await fs.readFile(configPath, "utf8"));
|
cookieSecret: process.env.COOKIE_SECRET || randomBytes(8).toString("hex"),
|
||||||
if (typeof userConfig.port === "number" && userConfig.port >= 0) config.port = userConfig.port;
|
mongoConnection: process.env.MONGO_URI || "mongodb://127.0.0.1",
|
||||||
if (typeof userConfig.cookieSecret === "string") config.cookieSecret = userConfig.cookieSecret;
|
mongoDatabase: (typeof process.env.MONGO_DB === "string" && process.env.MONGO_DB.length >= 2) ? process.env.MONGO_DB : undefined,
|
||||||
if (typeof userConfig.mongo === "object" && !(Array.isArray(userConfig.mongo))) {
|
portListen: Number(process.env.PORT || 3000),
|
||||||
if (typeof userConfig.mongo.databaseName === "string") config.mongo.databaseName = userConfig.mongo.databaseName;
|
ssh: {
|
||||||
if (typeof userConfig.mongo.uri === "string") config.mongo.uri = userConfig.mongo.uri;
|
port: Number(process.env.SSH_PORT || 3001),
|
||||||
|
hostKeys: sshKeys.length > 0 ? await Promise.all(sshKeys.map(async path => fs.readFile(path, "utf8"))) : [ (await createSSHKey()).privateKey ]
|
||||||
}
|
}
|
||||||
if (typeof userConfig.sshServer === "object" && !(Array.isArray(userConfig.sshServer))) {
|
};
|
||||||
if (typeof userConfig.sshServer.banner === "string") config.sshServer.banner = userConfig.sshServer.banner;
|
|
||||||
if (typeof userConfig.sshServer.port === "number" && userConfig.sshServer.port >= 0) config.sshServer.port = userConfig.sshServer.port;
|
|
||||||
if (Array.isArray(userConfig.sshServer.hostKeys) && userConfig.sshServer.hostKeys.length > 0) config.sshServer.hostKeys = userConfig.sshServer.hostKeys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(configPath, yaml.stringify(config));
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = await getConfig();
|
|
@ -1,99 +0,0 @@
|
|||||||
import cookie, { Store } from "express-session";
|
|
||||||
import { database } from "./db.js";
|
|
||||||
import { config } from "./config.js";
|
|
||||||
|
|
||||||
declare module "express-session" {
|
|
||||||
interface SessionData {
|
|
||||||
userID: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type cookieSave = {
|
|
||||||
sid: string;
|
|
||||||
session: cookie.SessionData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cookieCollection = database.collection<cookieSave>("authCookie");
|
|
||||||
class bdsSession extends Store {
|
|
||||||
nMap = new Map<string, cookie.SessionData>();
|
|
||||||
destroy(sid: string, callback?: (err?: any) => void): void {
|
|
||||||
if (this.nMap.has(sid)) this.nMap.delete(sid);
|
|
||||||
cookieCollection.deleteOne({ sid }).then(() => callback(), err => callback(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
get(sid: string, callback: (err?: any, session?: cookie.SessionData) => void) {
|
|
||||||
if (this.nMap.has(sid)) return callback(null, this.nMap.get(sid));
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const inDb = await cookieCollection.findOne({ sid });
|
|
||||||
if (inDb) return callback(null, inDb.session);
|
|
||||||
return callback();
|
|
||||||
} catch (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
load(sid: string, callback: (err?: any, session?: cookie.SessionData) => any): void {
|
|
||||||
if (this.nMap.has(sid)) return callback(null, this.nMap.get(sid));
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const inDb = await cookieCollection.findOne({ sid });
|
|
||||||
if (inDb) return callback(null, inDb.session);
|
|
||||||
return callback();
|
|
||||||
} catch (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
set(sid: string, session: cookie.SessionData, callback?: (err?: any) => void) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
if (this.nMap.has(sid)) return callback();
|
|
||||||
this.nMap.set(sid, session);
|
|
||||||
const existsInDb = await cookieCollection.findOne({ sid });
|
|
||||||
if (existsInDb) await cookieCollection.deleteOne({ sid });
|
|
||||||
await cookieCollection.insertOne({
|
|
||||||
sid,
|
|
||||||
session: typeof session["toJSON"] === "function" ? session["toJSON"]() : session,
|
|
||||||
});
|
|
||||||
this.nMap.delete(sid);
|
|
||||||
return callback();
|
|
||||||
} catch (err) {
|
|
||||||
callback(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
all(callback: (err: any, obj?: cookie.SessionData[] | { [sid: string]: cookie.SessionData; }) => void) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const sessions = await cookieCollection.find().toArray();
|
|
||||||
callback(null, sessions.reduce<Parameters<typeof callback>[1]>((acc, data) => {
|
|
||||||
acc[data.sid] = data.session;
|
|
||||||
return acc;
|
|
||||||
}, {}));
|
|
||||||
} catch (err) {
|
|
||||||
callback(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(callback?: (err?: any) => void) {
|
|
||||||
cookieCollection.deleteMany({}).then(() => callback(), err => callback(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default cookie({
|
|
||||||
name: "bdsLogin",
|
|
||||||
secret: config.cookieSecret,
|
|
||||||
resave: true,
|
|
||||||
saveUninitialized: true,
|
|
||||||
cookie: {
|
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 7 * 30,
|
|
||||||
httpOnly: false,
|
|
||||||
secure: "auto"
|
|
||||||
},
|
|
||||||
store: new bdsSession(),
|
|
||||||
});
|
|
5
packages/web/src/databaseConnect.ts
Normal file
5
packages/web/src/databaseConnect.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { MongoClient } from "mongodb";
|
||||||
|
import { localConfig } from "./config.js";
|
||||||
|
|
||||||
|
export const connection = await (new MongoClient(localConfig.mongoConnection)).connect();
|
||||||
|
export const mongoDatabase = connection.db(localConfig.mongoDatabase);
|
@ -1,163 +0,0 @@
|
|||||||
import { extendsFS } from "@sirherobrine23/extends";
|
|
||||||
import { bdsManegerRoot, runOptions, runServer, serverManegerV1 } from "@the-bds-maneger/core";
|
|
||||||
import { MongoClient } from "mongodb";
|
|
||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import util from "node:util";
|
|
||||||
import { getConfig } from "./config.js";
|
|
||||||
import { uniqueNamesGenerator, names, colors, animals, adjectives } from "unique-names-generator";
|
|
||||||
|
|
||||||
const config = await getConfig();
|
|
||||||
export const client = await (new MongoClient(config.mongo.uri)).connect();
|
|
||||||
export const database = client.db(config.mongo.databaseName);
|
|
||||||
|
|
||||||
export type userPermission = "root" | "admin" | "confirm";
|
|
||||||
export type userCollection = {
|
|
||||||
ID: string;
|
|
||||||
createAt: Date;
|
|
||||||
email: string;
|
|
||||||
password: {
|
|
||||||
salt: string;
|
|
||||||
hash: string;
|
|
||||||
};
|
|
||||||
username: string;
|
|
||||||
permissions: userPermission[];
|
|
||||||
tokens: string[];
|
|
||||||
sshKeys: {
|
|
||||||
private: string;
|
|
||||||
public: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userCollection = database.collection<userCollection>("user");
|
|
||||||
|
|
||||||
export async function createToken() {
|
|
||||||
let token: string;
|
|
||||||
function bufToChar(buf: Buffer) {
|
|
||||||
let str: string = "";
|
|
||||||
for (let i = 0; buf.length > i; i++) {
|
|
||||||
if ((/[a-zA-Z0-9]/).test(String.fromCharCode(buf[i]))) str += String.fromCharCode(buf[i]);
|
|
||||||
else str += crypto.randomInt(2, 20000);
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
while (true) {
|
|
||||||
if (await userCollection.findOne({ tokens: [(token = "tk_" + bufToChar(crypto.randomBytes(16)))] })) continue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSSHKey() {
|
|
||||||
const generateKeyPair = util.promisify(crypto.generateKeyPair);
|
|
||||||
return generateKeyPair("rsa", {
|
|
||||||
modulusLength: 3072,
|
|
||||||
privateKeyEncoding: {
|
|
||||||
format: "pem",
|
|
||||||
type: "pkcs1"
|
|
||||||
},
|
|
||||||
publicKeyEncoding: {
|
|
||||||
type: "pkcs1",
|
|
||||||
format: "pem"
|
|
||||||
}
|
|
||||||
}).then(a => ({ private: String(a.privateKey), public: String(a.publicKey) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function passworldSc(input: string): Promise<{ hash: string, salt: string }> {
|
|
||||||
const iv = crypto.randomBytes(16);
|
|
||||||
const secret = crypto.randomBytes(24);
|
|
||||||
return new Promise((done, reject) => {
|
|
||||||
crypto.scrypt(secret, "salt", 24, (err, key) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
const cipher = crypto.createCipheriv("aes-192-cbc", key, iv);
|
|
||||||
cipher.on("error", reject);
|
|
||||||
return done({
|
|
||||||
hash: Buffer.from(cipher.update(input, "utf8", "hex") + cipher.final("hex"), "utf8").toString("base64"),
|
|
||||||
salt: Buffer.from(iv.toString("hex") + "::::" + secret.toString("hex"), "utf8").toString("base64")
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function passworldDc(hash: string, salt: string): Promise<string> {
|
|
||||||
const hashSplit = Buffer.from(salt, "base64").toString("utf8").split("::::");
|
|
||||||
return new Promise((done, reject) => {
|
|
||||||
const iv = Buffer.from(hashSplit.at(0), "hex");
|
|
||||||
const secret = Buffer.from(hashSplit.at(1), "hex");
|
|
||||||
crypto.scrypt(secret, "salt", 24, (err, key) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
const decipher = crypto.createDecipheriv("aes-192-cbc", key, iv);
|
|
||||||
decipher.on("error", reject);
|
|
||||||
return done(decipher.update(Buffer.from(hash, "base64").toString(), "hex", "utf8") + decipher.final("utf8"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function passwordCheck(info: userCollection, password: string) {
|
|
||||||
const { password: { hash, salt } } = info;
|
|
||||||
return (await passworldDc(hash, salt)) === password;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type serverDB = {
|
|
||||||
ID: string;
|
|
||||||
platform: serverManegerV1["platform"];
|
|
||||||
name: string;
|
|
||||||
users: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serveCollection = database.collection<serverDB>("server");
|
|
||||||
export const serversIDs = serveCollection;
|
|
||||||
|
|
||||||
export async function getServerPaths(ID: string): Promise<serverManegerV1> {
|
|
||||||
const info = await serveCollection.findOne({ ID });
|
|
||||||
if (!(info)) throw new Error("Server not exists!");
|
|
||||||
|
|
||||||
const rootPath = path.join(bdsManegerRoot, info.platform, ID);
|
|
||||||
const serverFolder = path.join(rootPath, "server");
|
|
||||||
const backup = path.join(rootPath, "backups");
|
|
||||||
const log = path.join(rootPath, "logs");
|
|
||||||
|
|
||||||
// Create folders
|
|
||||||
for (const p of [serverFolder, backup, log]) if (!(await extendsFS.exists(p))) await fs.mkdir(p, { recursive: true });
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: ID,
|
|
||||||
platform: info.platform,
|
|
||||||
rootPath,
|
|
||||||
serverFolder,
|
|
||||||
backup,
|
|
||||||
logs: log,
|
|
||||||
async runCommand(options: Omit<runOptions, "cwd">) {
|
|
||||||
return runServer({
|
|
||||||
...options,
|
|
||||||
cwd: serverFolder
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createServerID(platform: serverManegerV1["platform"], usersIds: string[] = []): Promise<serverManegerV1> {
|
|
||||||
if (!((["bedrock", "java"]).includes(platform))) throw new Error("Set valid platform name!");
|
|
||||||
|
|
||||||
// Create Server ID
|
|
||||||
let ID: string;
|
|
||||||
while (true) {
|
|
||||||
if (await userCollection.findOne({ ID: (ID = crypto.randomUUID().split("-").join("_")) })) continue;
|
|
||||||
else if (await extendsFS.exists(path.join(bdsManegerRoot, platform, ID))) continue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert
|
|
||||||
await serveCollection.insertOne({
|
|
||||||
ID,
|
|
||||||
name: uniqueNamesGenerator({dictionaries: [ names, colors, animals, adjectives ]}),
|
|
||||||
platform,
|
|
||||||
users: []
|
|
||||||
});
|
|
||||||
|
|
||||||
// If seted user inject to DB
|
|
||||||
if (usersIds && usersIds.length > 0) await serveCollection.findOneAndUpdate({ ID }, { $set: { users: usersIds } });
|
|
||||||
|
|
||||||
return getServerPaths(ID);
|
|
||||||
}
|
|
@ -1,33 +1,25 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import "dotenv/config.js";
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import expressLayer from "express/lib/router/layer.js";
|
import http from "node:http";
|
||||||
import { config } from "./config.js";
|
import { cookie } from "./auth.js";
|
||||||
import cookie from "./cookie.js";
|
import { localConfig } from "./config.js";
|
||||||
import loginRegisterRoute from "./login.js";
|
import mcserver from "./mcserver.js";
|
||||||
import mcserverAPI from "./mcserver.js";
|
import { nextHandler, nextUpgarde } from "./reactServer.js";
|
||||||
import * as nextPage from "./reactServer.js";
|
|
||||||
import { server as sshServer } from "./ssh.js";
|
|
||||||
|
|
||||||
// Patch express promise catch's
|
|
||||||
expressLayer.prototype.handle_request = async function handle_request_promised(...args) {
|
|
||||||
var fn = this.handle;
|
|
||||||
if (fn.length > 3) return args.at(-1)();
|
|
||||||
await Promise.resolve().then(() => fn.call(this, ...args)).catch(args.at(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Express app
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const server = http.createServer();
|
||||||
|
server.on("upgrade", nextUpgarde);
|
||||||
|
server.on("request", app);
|
||||||
|
|
||||||
app.disable("etag").disable("x-powered-by");
|
app.disable("etag").disable("x-powered-by");
|
||||||
app.use(cookie, express.json(), express.urlencoded({ extended: true }));
|
app.use(cookie, express.json(), express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// API
|
// API 404
|
||||||
app.use("/api/mcserver", mcserverAPI);
|
app.use("/api/mcserver", mcserver);
|
||||||
app.use(loginRegisterRoute);
|
app.use("/api", ({res}) => res.status(404).json({error: "endpoint not exists!"}));
|
||||||
|
|
||||||
// Next request
|
// Page render
|
||||||
app.all("*", (req, res) => nextPage.nextHandler(req, res));
|
app.all("*", (req, res) => nextHandler(req, res));
|
||||||
|
|
||||||
// 500 error
|
// 500 error
|
||||||
app.use((err, _req, res, _next) => {
|
app.use((err, _req, res, _next) => {
|
||||||
@ -35,14 +27,8 @@ app.use((err, _req, res, _next) => {
|
|||||||
res.status(500).json({ error: err?.message || err })
|
res.status(500).json({ error: err?.message || err })
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(config.port, function () {
|
// Server listen
|
||||||
const addr = this.address();
|
server.listen(localConfig.portListen, () => {
|
||||||
console.log("Dashboard/API listen on %O", typeof addr === "object" ? addr.port : Number(addr));
|
const addr = server.address();
|
||||||
this.on("upgrade", nextPage.nextUpgarde);
|
console.log("HTTP Listen on %s", typeof addr === "object" ? addr.port : addr);
|
||||||
if (config.sshServer.port >= 0) {
|
|
||||||
sshServer.listen(config.sshServer.port, function listenOn() {
|
|
||||||
const addr = sshServer.address();
|
|
||||||
console.log("SSH listen on %O", typeof addr === "object" ? addr.port : Number(addr));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
@ -1,87 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
// import rateLimit from "express-rate-limit";
|
|
||||||
import crypto from "node:crypto";
|
|
||||||
import { createToken, passwordCheck, passworldSc, createSSHKey, userCollection } from "./db.js";
|
|
||||||
import { pageRender } from "./reactServer.js";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const app = express.Router();
|
|
||||||
export default app;
|
|
||||||
|
|
||||||
app.get("/login", (req, res) => {
|
|
||||||
return pageRender(req, res, "/login");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/register", (req, res) => {
|
|
||||||
return pageRender(req, res, "/register");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/login", async (req, res) => {
|
|
||||||
if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to login" });
|
|
||||||
const existsUser = await userCollection.findOne({ $or: [{ username: req.body.username }, { email: req.body.username }] });
|
|
||||||
if (!existsUser) return res.status(400).json({ error: "User not exists" });
|
|
||||||
else if (!(await passwordCheck(existsUser, req.body.password))) return res.status(401).json({ error: "Invalid password" });
|
|
||||||
req.session.userID = existsUser.ID;
|
|
||||||
await new Promise<void>((done, reject) => req.session.save(err => err ? reject(err) : done()));
|
|
||||||
return res.json({
|
|
||||||
ID: existsUser.ID,
|
|
||||||
createAt: existsUser.createAt,
|
|
||||||
username: existsUser.username,
|
|
||||||
permissions: existsUser.permissions,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete("/api/logout", async (req, res) => {
|
|
||||||
if (typeof req.session.userID === "string") await new Promise<void>((done, reject) => req.session.destroy(err => err ? reject(err) : done()));
|
|
||||||
return res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/register", async (req, res) => {
|
|
||||||
if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to register user!" });
|
|
||||||
const { username, email, password } = req.body;
|
|
||||||
if (!(typeof username === "string" && typeof email === "string")) return res.status(400).json({ error: "Invalid username and email body" });
|
|
||||||
else if (!(typeof password === "string" && (password.length >= 8))) return res.status(400).json({ error: "Require password with 8 characters" });
|
|
||||||
else if (await userCollection.findOne({ email})) return res.status(400).json({ error: "email in use!" });
|
|
||||||
else if (await userCollection.findOne({ username})) return res.status(400).json({ error: "username in use!" });
|
|
||||||
const passEncrypt = await passworldSc(password);
|
|
||||||
let ID: string;
|
|
||||||
while (true) if (!(await userCollection.findOne({ ID: (ID = crypto.randomUUID()) }))) break;
|
|
||||||
const token = await createToken();
|
|
||||||
const sshKey = await createSSHKey();
|
|
||||||
await userCollection.insertOne({
|
|
||||||
ID, createAt: new Date(),
|
|
||||||
email, username, password: passEncrypt,
|
|
||||||
permissions: [
|
|
||||||
"confirm"
|
|
||||||
],
|
|
||||||
tokens: [token],
|
|
||||||
sshKeys: [sshKey]
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(201).json({
|
|
||||||
ID,
|
|
||||||
token,
|
|
||||||
sshKey
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteIDs = new Map<string, string>();
|
|
||||||
app.delete("/api/register", async (req, res) => {
|
|
||||||
if (typeof req.session.userID !== "string") return res.status(400).json({ error: "Require login fist to delete account" });
|
|
||||||
let deleteID: string;
|
|
||||||
while (true) if (!(deleteIDs.has((deleteID = crypto.randomUUID())))) break;
|
|
||||||
deleteIDs.set(deleteID, req.session.userID);
|
|
||||||
const location = path.posix.join((new URL(req.url, "localhost.com")).pathname, deleteID);
|
|
||||||
res.setHeader("Location", location);
|
|
||||||
return res.status(201).json({
|
|
||||||
deleteID
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete("/api/register/:deleteID", async (req, res) => {
|
|
||||||
if (!(deleteIDs.has(req.params.deleteID))) return res.status(400).json({ error: "Id not exists!" });
|
|
||||||
else if (deleteIDs.get(req.params.deleteID) !== req.session.userID) return res.status(400).json({ error: "You do not have access to this ID" });
|
|
||||||
const userInfo = await userCollection.findOneAndDelete({ ID: req.session.userID });
|
|
||||||
deleteIDs.delete(req.params.deleteID);
|
|
||||||
return res.json(userInfo.value);
|
|
||||||
});
|
|
@ -1,132 +1,17 @@
|
|||||||
import { Bedrock, Java, serverRun } from "@the-bds-maneger/core";
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import fs from "node:fs/promises";
|
import { random } from "./auth.js";
|
||||||
import { createServerID, getServerPaths, passwordCheck, serversIDs, userCollection } from "./db.js";
|
import { mongoDatabase } from "./databaseConnect.js";
|
||||||
|
|
||||||
|
type serverStor = {
|
||||||
|
readonly ID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serverCollection = mongoDatabase.collection<serverStor>("servers");
|
||||||
|
export async function generateID() {
|
||||||
|
let ID: string;
|
||||||
|
while (true) if (!(await serverCollection.findOne({ID: (ID = random())}))) break;
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
const app = express.Router();
|
const app = express.Router();
|
||||||
export default app;
|
export default app;
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Express {
|
|
||||||
export interface Request {
|
|
||||||
userInfo?: userCollection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check token
|
|
||||||
app.use(async (req, res, next) => {
|
|
||||||
if (typeof req.headers.authorization === "string") {
|
|
||||||
const { authorization } = req.headers;
|
|
||||||
if (authorization.startsWith("Basic ")) {
|
|
||||||
const authDecrypt = Buffer.from(authorization.slice(5).trim(), "base64").toString("utf8");
|
|
||||||
const username = authDecrypt.slice(0, authDecrypt.indexOf(":"));
|
|
||||||
const password = authDecrypt.slice(authDecrypt.indexOf(":") + 1);
|
|
||||||
if (!(username && password)) return res.status(401).json({ error: "Basic auth require username and password!" });
|
|
||||||
const userInfo = await userCollection.findOne({ $or: [{ username }, { email: username }] });
|
|
||||||
if (!userInfo) return res.status(401).json({ error: "User not exists" });
|
|
||||||
else if (!(await passwordCheck(userInfo, password))) return res.status(401).json({ error: "Invalid password" });
|
|
||||||
req.session.userID = userInfo.ID;
|
|
||||||
} else if (authorization.startsWith("Token ") || authorization.startsWith("token ")) {
|
|
||||||
const token = authorization.slice(5).trim();
|
|
||||||
const userInfo = await userCollection.findOne({ tokens: [token] });
|
|
||||||
if (!userInfo) return res.status(401).json({ error: "Token not exists" });
|
|
||||||
req.session.userID = userInfo.ID;
|
|
||||||
} else return res.status(401).json({ error: "Invalid authorization schema" });
|
|
||||||
await new Promise<void>((done, reject) => req.session.save(err => err ? reject(err) : done()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof req.session.userID !== "string") return res.status(401).json({ error: "Not authenticated" });
|
|
||||||
const userInfo = req.userInfo = await userCollection.findOne({ ID: req.session.userID });
|
|
||||||
if (!userInfo) {
|
|
||||||
await new Promise<void>((done, reject) => req.session.destroy(err => err ? reject(err) : done()));
|
|
||||||
return res.status(401).json({ error: "User not exists" });
|
|
||||||
} else if (userInfo.permissions.includes("confirm")) return res.status(401).json({ error: "Unauthorized, ask the Site administrator for confirmation!" });
|
|
||||||
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sessionMAP = new Map<string, serverRun>();
|
|
||||||
|
|
||||||
// List auth user server allow access
|
|
||||||
app.get("/", async (req, res) => {
|
|
||||||
const servers = await serversIDs.find({ users: [req.session.userID] }).toArray();
|
|
||||||
return res.json(servers.map(info => ({
|
|
||||||
ID: info.ID,
|
|
||||||
platform: info.platform,
|
|
||||||
name: info.name,
|
|
||||||
running: sessionMAP.has(info.ID),
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create new Server
|
|
||||||
app.post("/", async (req, res) => {
|
|
||||||
if (!(req.userInfo.permissions.includes("admin"))) return res.status(401).json({ error: "You no have access to create server" });
|
|
||||||
else if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to setup server" });
|
|
||||||
const { platform } = req.body;
|
|
||||||
if (!(platform === "bedrock" || platform === "java")) return res.status(400).json({ error: "Invalid platform", body: req.body });
|
|
||||||
const v1 = await createServerID(platform, [req.session.userID]);
|
|
||||||
if (platform === "bedrock") {
|
|
||||||
await Bedrock.installServer(v1, { version: req.body.version, altServer: req.body.altServer, allowBeta: !!req.body.allowBeta });
|
|
||||||
} else {
|
|
||||||
await Java.installServer(v1, { version: req.body.version, altServer: req.body.altServer, allowBeta: !!req.body.allowBeta });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(201).json({
|
|
||||||
ID: v1.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete("/", async (req, res) => {
|
|
||||||
if (!(req.userInfo.permissions.includes("admin"))) return res.status(401).json({ error: "You no have access to create server" });
|
|
||||||
else if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to setup server" });
|
|
||||||
const serverInfo = await serversIDs.findOne({ ID: String(req.body.id) });
|
|
||||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
|
||||||
if (sessionMAP.has(serverInfo.ID)) await sessionMAP.get(serverInfo.ID).stopServer();
|
|
||||||
const v1 = await getServerPaths(serverInfo.ID);
|
|
||||||
await fs.rm(v1.rootPath, { recursive: true, force: true });
|
|
||||||
return res.json((await serversIDs.findOneAndDelete({ ID: serverInfo.ID })).value);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/server/:ID", async (req, res) => {
|
|
||||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
|
||||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
|
||||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
|
||||||
const Running = sessionMAP.get(serverInfo.ID);
|
|
||||||
return res.json({
|
|
||||||
running: !!Running,
|
|
||||||
ports: Running?.portListening,
|
|
||||||
players: Running?.playerActions,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/server/:ID/hotbackup", async (req, res) => {
|
|
||||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
|
||||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
|
||||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
|
||||||
else if (!(sessionMAP.has(req.params.ID))) return res.status(400).json({ error: "Server not running" });
|
|
||||||
const run = sessionMAP.get(serverInfo.ID);
|
|
||||||
const data = await run.hotBackup();
|
|
||||||
if (!data) return res.status(503).json({ error: "Server not support hot backup" });
|
|
||||||
return data.pipe(res.writeHead(200, {}));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/server/:ID", async (req, res) => {
|
|
||||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
|
||||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
|
||||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
|
||||||
else if (sessionMAP.has(serverInfo.ID)) return res.status(400).json({ error: "the server is already running" });
|
|
||||||
const v1 = await getServerPaths(serverInfo.ID);
|
|
||||||
const server = await (serverInfo.platform === "bedrock" ? Bedrock.startServer : Java.startServer)(v1, {});
|
|
||||||
sessionMAP.set(v1.id, server);
|
|
||||||
server.once("exit", () => sessionMAP.delete(v1.id));
|
|
||||||
return res.status(201).json({ ID: v1.id, pid: server.pid });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete("/server/:ID", async (req, res) => {
|
|
||||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
|
||||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
|
||||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
|
||||||
else if (!(sessionMAP.has(req.params.ID))) return res.status(400).json({ error: "Server not running" });
|
|
||||||
return res.json(await sessionMAP.get(req.params.ID).stopServer());
|
|
||||||
});
|
|
@ -1,24 +1,20 @@
|
|||||||
import _next, { NextConfig } from "next";
|
import _next, { NextConfig } from "next";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { config } from "./config.js";
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const next: typeof _next.default = _next as any;
|
const next: typeof _next.default = _next as any;
|
||||||
let _require: typeof require;
|
let _require = createRequire(import.meta.url);
|
||||||
if (typeof require === "function") _require = require; else _require = (await import("module")).default.createRequire(import.meta.url);
|
export const dev = import.meta.url.endsWith(".ts");
|
||||||
export const dev = import.meta.url.endsWith(".ts"), { HOSTNAME = "localhost" } = process.env;
|
|
||||||
|
|
||||||
const dir = path.join(__dirname, "next");
|
const dir = path.join(__dirname, "next");
|
||||||
const nextConfig: NextConfig = _require(path.join(dir, "next.config.cjs"));
|
const nextConfig: NextConfig = _require(path.join(dir, "next.config.cjs"));
|
||||||
nextConfig.env ||= {};
|
nextConfig.env ||= {};
|
||||||
nextConfig.env.SERVER_PORT = String(config.port)
|
|
||||||
|
|
||||||
export const nextApp = next({
|
export const nextApp = next({
|
||||||
customServer: true,
|
customServer: true,
|
||||||
hostname: HOSTNAME,
|
|
||||||
quiet: true,
|
quiet: true,
|
||||||
port: config.port,
|
|
||||||
conf: nextConfig,
|
conf: nextConfig,
|
||||||
dir,
|
dir,
|
||||||
dev,
|
dev,
|
||||||
@ -30,5 +26,5 @@ export const nextUpgarde = nextApp.getUpgradeHandler();
|
|||||||
export const {
|
export const {
|
||||||
render: pageRender,
|
render: pageRender,
|
||||||
render404,
|
render404,
|
||||||
renderError,
|
renderError
|
||||||
} = nextApp;
|
} = nextApp;
|
@ -1,51 +0,0 @@
|
|||||||
import ssh2 from "ssh2";
|
|
||||||
import { config } from "./config.js";
|
|
||||||
import { userCollection, serveCollection } from "./db.js";
|
|
||||||
import { sessionMAP } from "./mcserver.js";
|
|
||||||
|
|
||||||
export const server = new ssh2.Server({
|
|
||||||
hostKeys: config.sshServer.hostKeys,
|
|
||||||
banner: config.sshServer.banner,
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("error", err => console.error(err));
|
|
||||||
server.on("connection", client => {
|
|
||||||
let username: string;
|
|
||||||
client.on("error", err => console.error(err));
|
|
||||||
client.on("authentication", async ctx => {
|
|
||||||
username = ctx.username;
|
|
||||||
const serverInfo = await serveCollection.findOne({ID: username});
|
|
||||||
if (!serverInfo) return ctx.reject();
|
|
||||||
if (ctx.method === "hostbased" || ctx.method === "keyboard-interactive") return ctx.reject(["password", "publickey"]);
|
|
||||||
else if (ctx.method === "none") return ctx.reject(["password", "publickey"]);
|
|
||||||
else if (ctx.method === "publickey") {
|
|
||||||
const { key } = ctx;
|
|
||||||
const Users = await userCollection.find({ID: serverInfo.users}).toArray();
|
|
||||||
const userInfo = Users.find(user => user.sshKeys.find(userKey => {
|
|
||||||
const ps = ssh2.utils.parseKey(userKey.private);
|
|
||||||
if (ps instanceof Error) return false;
|
|
||||||
return Buffer.compare(ps.getPublicSSH(), key.data) === 0;
|
|
||||||
}));
|
|
||||||
if (!userInfo) return ctx.reject();
|
|
||||||
} else if (ctx.method === "password") {
|
|
||||||
const userInfo = await userCollection.findOne({$or: [serverInfo.users.map(ID => ({ID}))], tokens: [ctx.password]});
|
|
||||||
if (!userInfo) return ctx.reject();
|
|
||||||
}
|
|
||||||
return ctx.accept();
|
|
||||||
});
|
|
||||||
client.on("ready", () => {
|
|
||||||
client.on("session", (accept, reject) => {
|
|
||||||
if (!(sessionMAP.has(username))) return reject();
|
|
||||||
const serverSession = sessionMAP.get(username);
|
|
||||||
const session = accept();
|
|
||||||
serverSession.stdout.pipe(session.stdout, {end: false});
|
|
||||||
serverSession.stderr.pipe(session.stderr, {end: false});
|
|
||||||
serverSession.stdin.pipe(session.stdin, {end: false});
|
|
||||||
session.once("close", () => {
|
|
||||||
serverSession.stdout.unpipe(session.stdout);
|
|
||||||
serverSession.stderr.unpipe(session.stderr);
|
|
||||||
session.stdin.unpipe(serverSession.stdin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Reference in New Issue
Block a user