Web interface #525

Closed
Sirherobrine23 wants to merge 7 commits from web-interface into main
37 changed files with 1240 additions and 2 deletions
Showing only changes of commit acce3c1d0e - Show all commits

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# System
.DS_Store
# npm # npm
*.tgz *.tgz

View File

@ -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
View 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;
}

View File

@ -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();

View File

@ -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(),
});

View 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);

View File

@ -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);
}

View File

@ -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));
});
}
}); });

View File

@ -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);
});

View File

@ -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";
const app = express.Router(); type serverStor = {
export default app; readonly ID: string;
};
declare global { export const serverCollection = mongoDatabase.collection<serverStor>("servers");
namespace Express { export async function generateID() {
export interface Request { let ID: string;
userInfo?: userCollection; while (true) if (!(await serverCollection.findOne({ID: (ID = random())}))) break;
} return ID;
}
} }
// Check token const app = express.Router();
app.use(async (req, res, next) => { export default app;
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());
});

View File

@ -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;

View File

@ -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);
});
});
});
});