server framework
This commit is contained in:
parent
4e5531b2e2
commit
4e74d679cc
17 changed files with 4287 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
config.juml
|
3298
package-lock.json
generated
Normal file
3298
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
package.json
Normal file
37
package.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "outbag-server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Did you know that you can host your own outbag instance?",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"main": "tsc && node . -c config.juml"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://codeberg.org/outbag/server.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"outbag"
|
||||||
|
],
|
||||||
|
"author": "jusax23, comcloudway",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/pg": "^8.6.6",
|
||||||
|
"@types/prompts": "^2.4.2",
|
||||||
|
"esbuild": "^0.17.10",
|
||||||
|
"pkg": "^5.8.0",
|
||||||
|
"typescript": "^4.9.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^10.0.0",
|
||||||
|
"dblang": "https://jusax.de/git/attachments/c13552b7-c9f0-4f50-bcce-96a124c1c286",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"juml": "https://jusax.de/git/attachments/208913c5-2851-4b86-a53d-ca99fed168cc",
|
||||||
|
"nman": "https://jusax.de/git/attachments/5333948b-fe6b-45d2-9230-ca388f6a89bc",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"ws": "^8.12.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
# Outbag Server
|
# Outbag Server
|
||||||
|
Mybe outdated!!!
|
||||||
|
|
||||||
Did you know that you can host your own outbag instance?
|
Did you know that you can host your own outbag instance?
|
||||||
|
|
||||||
This repo contains the official outbag server.
|
This repo contains the official outbag server.
|
55
src/main.ts
Normal file
55
src/main.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
declare global {
|
||||||
|
var debug: boolean;
|
||||||
|
}
|
||||||
|
import express from "express";
|
||||||
|
import https from "https";
|
||||||
|
import http from "http";
|
||||||
|
import { Command } from "commander";
|
||||||
|
import { oConf } from "./sys/config.js"
|
||||||
|
import { log } from "./sys/log.js"
|
||||||
|
import { connectToDB } from "./sys/db.js"
|
||||||
|
import bruteforce from "./sys/bruteforce.js"
|
||||||
|
import { getSettings, setSettings } from "./sys/settings.js"
|
||||||
|
|
||||||
|
import { fullSetup, partiellSetup } from "./setup/config.js"
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
version: "0.0.1"
|
||||||
|
};
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name("Outbag Server")
|
||||||
|
.description("The default way to host your own Outbag server.")
|
||||||
|
.version(config.version);
|
||||||
|
program
|
||||||
|
.option("-c, --config <path>", "Start the Outbag server with a config file.")
|
||||||
|
.option("-d, --debug", "Start the Outbag server with more log output.")
|
||||||
|
.option("-s, --setup", "Start the Server Setup Tool")
|
||||||
|
.action(({ config, debug, setup }) => {
|
||||||
|
let dofullSetup = false;
|
||||||
|
global.debug = debug != null;
|
||||||
|
if (config) {
|
||||||
|
log("System", "Starting with config:", config);
|
||||||
|
if(!oConf.connect(config)) dofullSetup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(setup || dofullSetup){
|
||||||
|
if(dofullSetup) fullSetup();
|
||||||
|
else partiellSetup();
|
||||||
|
}else{
|
||||||
|
startServer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
program.parse();
|
||||||
|
|
||||||
|
|
||||||
|
async function startServer() {
|
||||||
|
await connectToDB();
|
||||||
|
|
||||||
|
const server = express();
|
||||||
|
server.use(bruteforce);
|
||||||
|
}
|
58
src/server/outbagURL.ts
Normal file
58
src/server/outbagURL.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
const WELL_KNOWN_PATH = "/.well-known/outbag/server";
|
||||||
|
const DEFAULT_PORT = 7223;
|
||||||
|
|
||||||
|
export const outbagURL = (url: string, prefix = "https") => new Promise((res, rej) => {
|
||||||
|
let uri: URL;
|
||||||
|
try {
|
||||||
|
uri = new URL("/", url);
|
||||||
|
uri.protocol = "https";
|
||||||
|
} catch (_) {
|
||||||
|
uri = new URL("https://" + url);
|
||||||
|
}
|
||||||
|
uri.pathname = WELL_KNOWN_PATH;
|
||||||
|
fetch(uri)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(({ path = "/", port = 7223 }) => {
|
||||||
|
uri.pathname = path;
|
||||||
|
uri.port = port;
|
||||||
|
uri.protocol = prefix;
|
||||||
|
res(uri.toString());
|
||||||
|
})
|
||||||
|
.catch(_ => {
|
||||||
|
if (uri.port == '') {
|
||||||
|
uri.port = DEFAULT_PORT+"";
|
||||||
|
outbagURL(uri.toString(), prefix)
|
||||||
|
.then(url => res(url))
|
||||||
|
.catch(_ => rej());
|
||||||
|
} else {
|
||||||
|
rej();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
export const outbagURLshort = (url: string) => new Promise((res, rej) => {
|
||||||
|
let uri: URL;
|
||||||
|
try {
|
||||||
|
uri = new URL("/", url);
|
||||||
|
uri.protocol = "https";
|
||||||
|
} catch (_) {
|
||||||
|
uri = new URL("https://" + url);
|
||||||
|
}
|
||||||
|
uri.pathname = WELL_KNOWN_PATH;
|
||||||
|
fetch(uri)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(({ path = "/", port = 7223 }) => {
|
||||||
|
uri.pathname = path;
|
||||||
|
uri.port = port;
|
||||||
|
res(uri.hostname + ":" + uri.port);
|
||||||
|
})
|
||||||
|
.catch(_ => {
|
||||||
|
if (uri.port == '') {
|
||||||
|
uri.port = DEFAULT_PORT+"";
|
||||||
|
outbagURLshort(uri.toString())
|
||||||
|
.then(url => res(url))
|
||||||
|
.catch(_ => rej());
|
||||||
|
} else {
|
||||||
|
rej();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
16
src/server/permissions.ts
Normal file
16
src/server/permissions.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export const PERMISSIONS = {
|
||||||
|
NONE: 0b0000000000000000, // equal to no account or blocked account
|
||||||
|
|
||||||
|
DEFAULT: 0b0000000000000011, //default
|
||||||
|
|
||||||
|
CAN_USE_API: 0b0000000000000001,
|
||||||
|
PROVIDE_CERT: 0b0000000000000010,
|
||||||
|
|
||||||
|
MANAGE_OTA_TOKENS: 0b0000010000000000,
|
||||||
|
MANAGE_SERVER_PRODUKT_LIST: 0b0000100000000000,
|
||||||
|
SHOW_USERS_AND_LISTS: 0b0001000000000000,
|
||||||
|
EDIT_SETTINGS: 0b0010000000000000,
|
||||||
|
EDIT_RIGHTS: 0b0100000000000000,
|
||||||
|
EDIT_USERS: 0b1000000000000000,
|
||||||
|
ALL: 0b1111111111111111,
|
||||||
|
};
|
54
src/server/serverCerts.ts
Normal file
54
src/server/serverCerts.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { outbagURL } from "./outbagURL.js";
|
||||||
|
import { db, serverCerts } from "../sys/db.js"
|
||||||
|
import { eq, exists, insert, not, select, update } from "dblang";
|
||||||
|
import { error } from "../sys/log.js";
|
||||||
|
import { uts } from "../sys/tools.js"
|
||||||
|
|
||||||
|
|
||||||
|
async function updateRemote(url: string, pKey: string = ""): Promise<boolean | string> {
|
||||||
|
var urlP = await outbagURL(url);
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
fetch(urlP + "api/server/publicKey")
|
||||||
|
.then(d => d.json())
|
||||||
|
.then(async json => {
|
||||||
|
let { publicKey, expires } = json;
|
||||||
|
if (typeof publicKey == "string" && typeof expires == "string") {
|
||||||
|
try {
|
||||||
|
await insert(serverCerts.url, serverCerts.publicKey, serverCerts.expires)
|
||||||
|
.add(url, publicKey, expires)
|
||||||
|
.query(db);
|
||||||
|
} catch (error) {
|
||||||
|
await update(serverCerts)
|
||||||
|
.set(serverCerts.publicKey, publicKey)
|
||||||
|
.set(serverCerts.expires, expires)
|
||||||
|
.where(eq(serverCerts.url, url))
|
||||||
|
.query(db);
|
||||||
|
}
|
||||||
|
res(publicKey);
|
||||||
|
} else {
|
||||||
|
if (publicKey.length > 0) {
|
||||||
|
res(publicKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
error("serverCert", "fetch error:", e);
|
||||||
|
if (pKey.length > 0) {
|
||||||
|
res(pKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res(false);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRemote = async (url: string) => {
|
||||||
|
let query = await select([serverCerts.publicKey, serverCerts.expires], serverCerts)
|
||||||
|
.where(eq(serverCerts.url, url))
|
||||||
|
.query(db);
|
||||||
|
if (query.length == 0 || query[0][serverCerts.expires] < uts() - 60)
|
||||||
|
return await updateRemote(url, query[0][serverCerts.publicKey]);
|
||||||
|
return query[0][serverCerts.publicKey];
|
||||||
|
}
|
335
src/setup/config.ts
Normal file
335
src/setup/config.ts
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
import fs from "fs";
|
||||||
|
import { shutdown } from "nman";
|
||||||
|
import prompts from "prompts";
|
||||||
|
import { oConf } from "../sys/config.js";
|
||||||
|
import { connectToDB, connectToDBCredentials, db } from "../sys/db.js";
|
||||||
|
import { log, error, warn } from "../sys/log.js";
|
||||||
|
|
||||||
|
const isFile = async (path: string) => {
|
||||||
|
try {
|
||||||
|
if (path.startsWith("http")) {
|
||||||
|
let req = await fetch(path);
|
||||||
|
return req.status == 200;
|
||||||
|
} else {
|
||||||
|
await fs.promises.access(path, fs.constants.F_OK);
|
||||||
|
let state = await fs.promises.stat(path);
|
||||||
|
return state.isFile();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = () => {
|
||||||
|
var P = ["\\", "|", "/", "-"];
|
||||||
|
var x = 0;
|
||||||
|
let intID = setInterval(function () {
|
||||||
|
process.stdout.write("\r" + P[(x++) % P.length]);
|
||||||
|
}, 150);
|
||||||
|
return () => {
|
||||||
|
process.stdout.write("\r");
|
||||||
|
clearInterval(intID)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const system = async () => {
|
||||||
|
log("Setup", "Setting up generell System Settings:");
|
||||||
|
const resp = await prompts([
|
||||||
|
{
|
||||||
|
type: "number",
|
||||||
|
name: "port",
|
||||||
|
message: "Please enter the Port the Server should listen on!",
|
||||||
|
initial: oConf.get("System", "PORT")
|
||||||
|
}, {
|
||||||
|
type: "number",
|
||||||
|
name: "portexposed",
|
||||||
|
message: "Please enter the Port on which the server is exposed!",
|
||||||
|
initial: oConf.get("System", "PORTexposed")
|
||||||
|
}, {
|
||||||
|
type: "text",
|
||||||
|
name: "pathexposed",
|
||||||
|
message: "Please enter the Path on which the server is exposed!",
|
||||||
|
initial: oConf.get("System", "PATHexposed")
|
||||||
|
}, {
|
||||||
|
type: "text",
|
||||||
|
name: "url",
|
||||||
|
message: "Please enter the host name of the server!",
|
||||||
|
initial: oConf.get("System", "URL")
|
||||||
|
}, {
|
||||||
|
type: "number",
|
||||||
|
name: "certlivesec",
|
||||||
|
message: "Please enter the number of days after the Server Certificate should be expire!",
|
||||||
|
initial: oConf.get("System", "CertLiveSec") / 60 / 60 / 24
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
if (
|
||||||
|
resp.port == null
|
||||||
|
|| resp.portexposed == null
|
||||||
|
|| resp.pathexposed == null
|
||||||
|
|| resp.url == null
|
||||||
|
|| resp.certlivesec == null
|
||||||
|
) {
|
||||||
|
warn("Setup", "You have to provide all informations!");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp2 = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "correct",
|
||||||
|
message: "Are the Details correct?",
|
||||||
|
initial: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp2.correct) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oConf.set("System", "PORT", resp.port);
|
||||||
|
oConf.set("System", "PORTexposed", resp.portexposed);
|
||||||
|
oConf.set("System", "PATHexposed", resp.pathexposed);
|
||||||
|
oConf.set("System", "URL", resp.url);
|
||||||
|
oConf.set("System", "CertLiveSec", Math.round(resp.certlivesec * 24 * 60 * 60));
|
||||||
|
oConf.save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssl = async () => {
|
||||||
|
log("Setup", "Setting up ssl server:");
|
||||||
|
const respActive = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "ssl",
|
||||||
|
message: "Activate SSL? (If deactivated Server will run in test mode!)",
|
||||||
|
initial: true
|
||||||
|
});
|
||||||
|
if (respActive.ssl == null) return false;
|
||||||
|
if (respActive.ssl == false) {
|
||||||
|
oConf.set("ssl", "enable", false);
|
||||||
|
}
|
||||||
|
const resp = await prompts([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "privkey",
|
||||||
|
message: "Please enter path or link to privkey.pem",
|
||||||
|
validate: async (path: string) => await isFile(path) ? true : "Please enter a path or a link to a readable File!",
|
||||||
|
initial: oConf.get("ssl", "privkey")
|
||||||
|
}, {
|
||||||
|
type: "text",
|
||||||
|
name: "cert",
|
||||||
|
message: "Please enter path or link to cert.pem",
|
||||||
|
validate: async (path: string) => await isFile(path) ? true : "Please enter a path or a link to a readable File!",
|
||||||
|
initial: oConf.get("ssl", "cert")
|
||||||
|
}, {
|
||||||
|
type: "text",
|
||||||
|
name: "chain",
|
||||||
|
message: "Please enter path or link to chain.pem",
|
||||||
|
validate: async (path: string) => await isFile(path) ? true : "Please enter a path or a link to a readable File!",
|
||||||
|
initial: oConf.get("ssl", "chain")
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
resp.privkey == null
|
||||||
|
|| resp.cert == null
|
||||||
|
|| resp.chain == null
|
||||||
|
) {
|
||||||
|
warn("Setup", "You have to provide all informations!");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp2 = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "correct",
|
||||||
|
message: "Are the Details correct?",
|
||||||
|
initial: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp2.correct) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oConf.set("ssl", "enable", true);
|
||||||
|
oConf.set("ssl", "privkey", resp.privkey);
|
||||||
|
oConf.set("ssl", "cert", resp.cert);
|
||||||
|
oConf.set("ssl", "chain", resp.chain);
|
||||||
|
oConf.save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = async () => {
|
||||||
|
log("Setup", "Setting up Database:");
|
||||||
|
const resp = await prompts([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "host",
|
||||||
|
message: "Please enter database host!",
|
||||||
|
initial: oConf.get("Database", "host")
|
||||||
|
}, {
|
||||||
|
type: "number",
|
||||||
|
name: "port",
|
||||||
|
message: "Please enter database port!",
|
||||||
|
initial: oConf.get("Database", "port")
|
||||||
|
}, {
|
||||||
|
type: "text",
|
||||||
|
name: "user",
|
||||||
|
message: "Please enter the database username!",
|
||||||
|
initial: oConf.get("Database", "user")
|
||||||
|
}, {
|
||||||
|
type: "password",
|
||||||
|
name: "password",
|
||||||
|
message: "Please enter the database password!",
|
||||||
|
initial: oConf.get("Database", "password")
|
||||||
|
}, {
|
||||||
|
type: "text",
|
||||||
|
name: "database",
|
||||||
|
message: "Please enter the Outbag database Name!",
|
||||||
|
initial: oConf.get("Database", "database")
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
if (resp.host == null || resp.port == null || resp.user == null || resp.password == null || resp.database == null) {
|
||||||
|
warn("Setup", "You have to provide all informations!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp2 = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "correct",
|
||||||
|
message: "Are the Details correct?",
|
||||||
|
initial: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp2.correct) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Setup", "Trying to connect to database: ");
|
||||||
|
let cancle = loading();
|
||||||
|
try {
|
||||||
|
await connectToDBCredentials(resp.host, resp.port, resp.user, resp.password, resp.database);
|
||||||
|
cancle();
|
||||||
|
log("Setup", "Successful connected to Database!");
|
||||||
|
oConf.set("Database", "host", resp.host);
|
||||||
|
oConf.set("Database", "port", resp.port);
|
||||||
|
oConf.set("Database", "user", resp.user);
|
||||||
|
oConf.set("Database", "password", resp.password);
|
||||||
|
oConf.set("Database", "database", resp.database);
|
||||||
|
oConf.save();
|
||||||
|
db.close();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
cancle();
|
||||||
|
error("Setup", "Error while connecting to Database!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outbag = async () => {
|
||||||
|
log("Setup", "Setting up generell Outbag Settings:");
|
||||||
|
const resp = await prompts([
|
||||||
|
{
|
||||||
|
type: "number",
|
||||||
|
name: "maxUsers",
|
||||||
|
message: "Please enter the maximum amount of server users!",
|
||||||
|
initial: oConf.get("Settings", "maxUsers"),
|
||||||
|
validate: i => i >= -1
|
||||||
|
}, {
|
||||||
|
type: "number",
|
||||||
|
name: "defaultMaxRooms",
|
||||||
|
message: "Please enter the default maximum nuber of rooms!",
|
||||||
|
initial: oConf.get("Settings", "defaultMaxRooms"),
|
||||||
|
validate: i => i >= -1
|
||||||
|
}, {
|
||||||
|
type: "number",
|
||||||
|
name: "defaultMaxRoomSize",
|
||||||
|
message: "Please enter the default maximum amount of Elements in one Room!",
|
||||||
|
initial: oConf.get("Settings", "defaultMaxRoomSize"),
|
||||||
|
validate: i => i >= -1
|
||||||
|
}, {
|
||||||
|
type: "number",
|
||||||
|
name: "defaultMaxUsersPerRoom",
|
||||||
|
message: "Please enter default maximum number of Users per Room!",
|
||||||
|
initial: oConf.get("Settings", "defaultMaxUsersPerRoom"),
|
||||||
|
validate: i => i >= -1
|
||||||
|
},
|
||||||
|
|
||||||
|
]);
|
||||||
|
if (
|
||||||
|
resp.maxUsers == null
|
||||||
|
|| resp.defaultMaxRooms == null
|
||||||
|
|| resp.defaultMaxRoomSize == null
|
||||||
|
|| resp.defaultMaxUsersPerRoom == null
|
||||||
|
) {
|
||||||
|
warn("Setup", "You have to provide all informations!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const resp2 = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "correct",
|
||||||
|
message: "Are the Details correct?",
|
||||||
|
initial: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp2.correct) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oConf.set("Settings", "maxUsers", resp.maxUsers);
|
||||||
|
oConf.set("Settings", "defaultMaxRooms", resp.defaultMaxRooms);
|
||||||
|
oConf.set("Settings", "defaultMaxRoomSize", resp.defaultMaxRoomSize);
|
||||||
|
oConf.set("Settings", "defaultMaxUsersPerRoom", resp.defaultMaxUsersPerRoom);
|
||||||
|
oConf.save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupFunc = async (func: () => Promise<boolean>, name: string) => {
|
||||||
|
let dbConn = await func();
|
||||||
|
while (!dbConn) {
|
||||||
|
console.log();
|
||||||
|
const resp = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "again",
|
||||||
|
message: "Try to setup '" + name + "' again?",
|
||||||
|
initial: false
|
||||||
|
});
|
||||||
|
if (!resp.again) break;
|
||||||
|
dbConn = await func();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const partiellSetup = async () => {
|
||||||
|
while (true) {
|
||||||
|
log("Setup", "Slection:");
|
||||||
|
const resp = await prompts({
|
||||||
|
type: 'select',
|
||||||
|
name: 'value',
|
||||||
|
message: 'Which Settings do you want to change?',
|
||||||
|
choices: [
|
||||||
|
{ title: 'System', description: 'Setup generell System Settings.' },
|
||||||
|
{ title: 'SSL', description: 'Setup ssl details.' },
|
||||||
|
{ title: 'Database', description: 'Setup Database connection.' },
|
||||||
|
{ title: 'Outbag', description: 'Change generell Outbag Settings.' },
|
||||||
|
],
|
||||||
|
initial: 0
|
||||||
|
});
|
||||||
|
if (resp.value == 0) await setupFunc(system, 'System');
|
||||||
|
else if (resp.value == 1) await setupFunc(ssl, 'SSL');
|
||||||
|
else if (resp.value == 2) await setupFunc(database, 'Database');
|
||||||
|
else if (resp.value == 3) await setupFunc(outbag, 'Outbag');
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
await shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fullSetup = async () => {
|
||||||
|
log("Setup", "Starting full setup script:");
|
||||||
|
await setupFunc(system, 'System');
|
||||||
|
console.log();
|
||||||
|
await setupFunc(ssl, 'SSL');
|
||||||
|
console.log();
|
||||||
|
await setupFunc(database, 'Database');
|
||||||
|
console.log();
|
||||||
|
await setupFunc(outbag, 'Outbag');
|
||||||
|
log("Setup", "Finished setup script!");
|
||||||
|
await shutdown();
|
||||||
|
}
|
51
src/sys/bruteforce.ts
Normal file
51
src/sys/bruteforce.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { addShutdownTask } from "nman";
|
||||||
|
import { log } from "./log.js"
|
||||||
|
import { uts } from "./tools.js";
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
const timeout = 10;
|
||||||
|
const deleteater = 600;
|
||||||
|
const maxSus = 100;
|
||||||
|
|
||||||
|
var bruteforcedata: { [key: string]: { n: number, t: number } } = {};
|
||||||
|
|
||||||
|
export const addBruteforcePotantial = (ip: string) => {
|
||||||
|
if (bruteforcedata[ip] == null) {
|
||||||
|
bruteforcedata[ip] = { n: 0, t: uts() };
|
||||||
|
log("Bruteforce Protection", "add ip: ", ip);
|
||||||
|
}
|
||||||
|
bruteforcedata[ip].n++;
|
||||||
|
bruteforcedata[ip].t = uts();
|
||||||
|
if (bruteforcedata[ip].n > maxSus) {
|
||||||
|
log("Bruteforce Protection", "blocking ip: ", ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bruteforcecheck = (ip: string) => {
|
||||||
|
return (bruteforcedata[ip]?.n || 0) <= maxSus || uts() - (bruteforcedata[ip]?.t || uts()) > timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bruteforcedatacleaner = setInterval(async () => {
|
||||||
|
var utst = uts();
|
||||||
|
let keys = Object.keys(bruteforcedata);
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
if (utst - bruteforcedata[keys[i]].t > deleteater) {
|
||||||
|
log("Bruteforce Protection", "remove ip: ", keys[i]);
|
||||||
|
delete bruteforcedata[keys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000 * 60);
|
||||||
|
|
||||||
|
addShutdownTask(() => clearInterval(bruteforcedatacleaner), 5000);
|
||||||
|
|
||||||
|
export interface suspectRequest extends express.Request {
|
||||||
|
suspect?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (req: suspectRequest, res: express.Response, next: express.NextFunction) => {
|
||||||
|
if (!bruteforcecheck(req.ip)) return void res.status(400).send("bruteforce");
|
||||||
|
req.suspect = () => {
|
||||||
|
addBruteforcePotantial(req.ip);
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
}
|
32
src/sys/config.ts
Normal file
32
src/sys/config.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import juml from "juml";
|
||||||
|
|
||||||
|
const conf_struct = {
|
||||||
|
System: {
|
||||||
|
PORT: { type: "number", default: 7223, env: "OUTBAG_PORT", comment:"The Server will listen on this Port!" },
|
||||||
|
PORTexposed: { type: "number", default: 7223, env: "OUTBAG_EXPOSED_PORT" },
|
||||||
|
PATHexposed: { type: "string", default: "/", env: "OUTBAG_EXPOSED_PATH" },
|
||||||
|
URL: { type: "string", default: "localhost", env: "OUTBAG_HOST" },
|
||||||
|
CertLiveSec: { type: "number", default: 60*60*24*30, env: "OUTBAG_CERT_LIVE_SEC" },
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
enable: { type: "boolean", default: false, env: "OUTBAG_SSL_ENABLED" },
|
||||||
|
privkey: { type: "string", default: "privkey.pem", env: "OUTBAG_SSL_PRIVATE_KEY" },
|
||||||
|
cert: { type: "string", default: "cert.pem", env: "OUTBAG_SSL_CERT" },
|
||||||
|
chain: { type: "string", default: "chain.pem", env: "OUTBAG_SSL_CHAIN" }
|
||||||
|
},
|
||||||
|
Database: {
|
||||||
|
host: { type: "string", default: "localhost", env: "OUTBAG_MYSQL_HOST" },
|
||||||
|
port: { type: "number", default: 3306, env: "OUTBAG_MYSQL_PORT" },
|
||||||
|
user: { type: "string", default: "admin", env: "OUTBAG_MYSQL_USER" },
|
||||||
|
password: { type: "string", default: "", env: "OUTBAG_MYSQL_PASSWORD" },
|
||||||
|
database: { type: "string", default: "outbag", env: "OUTBAG_MYSQL_DATABASE" }
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
maxUsers: { type:"number", default:0, env: "OUTBAG_MAX_USERS"},//Infinity = -1
|
||||||
|
defaultMaxRooms: { type:"number", default:3, env: "OUTBAG_DEFAULT_MAX_ROOMS"},//Infinity = -1
|
||||||
|
defaultMaxRoomSize: { type:"number", default:10000, env: "OUTBAG_DEFAULT_MAX_ROOMS_SIZE"},//Infinity = -1
|
||||||
|
defaultMaxUsersPerRoom: { type:"number", default:5, env: "OUTBAG_DEFAULT_MAX_USERS_PER_ROOM"},//Infinity = -1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const oConf = new juml(conf_struct);
|
57
src/sys/crypto.ts
Normal file
57
src/sys/crypto.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
export const sha256 = (d: any) => crypto.createHash('sha256').update(String(d)).digest('base64');
|
||||||
|
|
||||||
|
export const encode = (data: any, type: BufferEncoding = "binary") => {
|
||||||
|
return new Uint8Array(Buffer.from(data, type));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decode = (data: any, type: BufferEncoding = "binary") => {
|
||||||
|
return Buffer.from(data).toString(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateSigningKey = async () => {
|
||||||
|
var keyPair = await crypto.webcrypto.subtle.generateKey(
|
||||||
|
{ name: "RSA-PSS", modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
|
||||||
|
true,
|
||||||
|
["sign", "verify"]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
privateKey: decode(new Uint8Array(await crypto.webcrypto.subtle.exportKey("pkcs8", keyPair.privateKey)), "base64"),
|
||||||
|
publicKey: decode(new Uint8Array(await crypto.webcrypto.subtle.exportKey("spki", keyPair.publicKey)), "base64")
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sign = async (message: string, privateKey: string) => {
|
||||||
|
var rawdata = encode(message);
|
||||||
|
|
||||||
|
var rawkey = await crypto.webcrypto.subtle.importKey("pkcs8", encode(privateKey, "base64"), { name: "RSA-PSS", hash: "SHA-256" }, true, ["sign"]);
|
||||||
|
var step = await crypto.webcrypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: "RSA-PSS",
|
||||||
|
saltLength: 32,
|
||||||
|
},
|
||||||
|
rawkey,
|
||||||
|
rawdata
|
||||||
|
);
|
||||||
|
|
||||||
|
return decode(new Uint8Array(step), "base64");
|
||||||
|
};
|
||||||
|
export const verify = async (message: string, signature: string, publicKey: string) => {
|
||||||
|
var rawdata = encode(message);
|
||||||
|
var rawsig = encode(signature, "base64");
|
||||||
|
|
||||||
|
var rawkey = await crypto.webcrypto.subtle.importKey("spki", encode(publicKey, "base64"), { name: "RSA-PSS", hash: "SHA-256" }, true, ["verify"]);
|
||||||
|
var step = await crypto.webcrypto.subtle.verify(
|
||||||
|
{
|
||||||
|
name: "RSA-PSS",
|
||||||
|
saltLength: 32,
|
||||||
|
},
|
||||||
|
rawkey,
|
||||||
|
rawsig,
|
||||||
|
rawdata
|
||||||
|
);
|
||||||
|
|
||||||
|
return step//; ? "valid" : "invalid";
|
||||||
|
}
|
179
src/sys/db.ts
Normal file
179
src/sys/db.ts
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
import { BIGINT, BOOL, DB, INT, onAction, SMALLINT, TEXT, TINYINT, VARCHAR } from "dblang"
|
||||||
|
import { oConf } from "./config.js"
|
||||||
|
import { PERMISSIONS } from "../server/permissions.js"
|
||||||
|
import nMan from "nman";
|
||||||
|
import { log, error } from "./log.js";
|
||||||
|
|
||||||
|
export const db = new DB();
|
||||||
|
|
||||||
|
export const connectToDB = async () => {
|
||||||
|
db.connect({
|
||||||
|
host: oConf.get("Database", "host"),
|
||||||
|
port: oConf.get("Database", "port"),
|
||||||
|
user: oConf.get("Database", "user"),
|
||||||
|
password: oConf.get("Database", "password"),
|
||||||
|
database: oConf.get("Database", "database"),
|
||||||
|
});
|
||||||
|
accounts.deleted.ops.maxRooms = oConf.get("Settings", "defaultMaxRooms");
|
||||||
|
accounts.deleted.ops.maxRoomSize = oConf.get("Settings", "defaultMaxRoomSize");
|
||||||
|
accounts.deleted.ops.maxUsersPerRoom = oConf.get("Settings", "defaultMaxUsersPerRoom");
|
||||||
|
try {
|
||||||
|
await db.sync(true);
|
||||||
|
log("Database", "Connected to Database!");
|
||||||
|
} catch (e) {
|
||||||
|
error("Database", "Error while conncting to Database!");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectToDBCredentials = async (host: string, port: number, user: string, password: string, database: string) => {
|
||||||
|
db.connect({
|
||||||
|
host, port, user, password, database,
|
||||||
|
});
|
||||||
|
accounts.deleted.ops.maxRooms = oConf.get("Settings", "defaultMaxRooms");
|
||||||
|
accounts.deleted.ops.maxRoomSize = oConf.get("Settings", "defaultMaxRoomSize");
|
||||||
|
accounts.deleted.ops.maxUsersPerRoom = oConf.get("Settings", "defaultMaxUsersPerRoom");
|
||||||
|
try {
|
||||||
|
await db.sync(true);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nMan.addShutdownTask(db.close, 3000, 10);
|
||||||
|
|
||||||
|
export const accounts = db.newTable("accounts");
|
||||||
|
accounts.addAttributes({
|
||||||
|
accID: { type: INT, primaryKey: true, autoIncrement: true },
|
||||||
|
name: { type: VARCHAR(255), default: PERMISSIONS.DEFAULT },
|
||||||
|
|
||||||
|
accountKeySalt: { type: VARCHAR(64) },
|
||||||
|
accountKey: { type: VARCHAR(64) },
|
||||||
|
|
||||||
|
viewable: { type: BOOL, default: true },
|
||||||
|
|
||||||
|
deleted: { type: BOOL, default: false },
|
||||||
|
|
||||||
|
maxRooms: { type: INT },
|
||||||
|
maxRoomSize: { type: INT },
|
||||||
|
maxUsersPerRoom: { type: INT },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const settings = db.newTable("settings");
|
||||||
|
settings.addAttributes({
|
||||||
|
type: { type: VARCHAR(255), primaryKey: true },
|
||||||
|
data: { type: TEXT },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const serverCerts = db.newTable("serverCerts");
|
||||||
|
serverCerts.addAttributes({
|
||||||
|
serverCertID: { type: INT, primaryKey: true, autoIncrement: true },
|
||||||
|
url: { type: TEXT },
|
||||||
|
publicKey: { type: TEXT },
|
||||||
|
expires: { type: BIGINT }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const signupOTA = db.newTable("signupOTA");
|
||||||
|
signupOTA.addAttributes({
|
||||||
|
token: { type: VARCHAR(128), primaryKey: true },
|
||||||
|
expires: { type: BIGINT },
|
||||||
|
usesLeft: { type: INT }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rooms = db.newTable("rooms");
|
||||||
|
rooms.addAttributes({
|
||||||
|
roomID: { type: INT, primaryKey: true, autoIncrement: true },
|
||||||
|
name: { type: VARCHAR(255), unique: true },
|
||||||
|
owner: {
|
||||||
|
type: INT,
|
||||||
|
foreginKey: {
|
||||||
|
link: accounts.accID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rights: { type: INT, default: 0b11111 },
|
||||||
|
public: { type: BOOL, default: 0 },
|
||||||
|
title: { type: TEXT, default: "" },
|
||||||
|
description: { type: TEXT, default: "" },
|
||||||
|
icon: { type: TEXT, default: "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roomMembers = db.newTable("roomMembers");
|
||||||
|
roomMembers.addAttributes({
|
||||||
|
roomMemberID: { type: INT, primaryKey: true, autoIncrement: true },
|
||||||
|
roomID: {
|
||||||
|
type: INT,
|
||||||
|
foreginKey: {
|
||||||
|
link: rooms.roomID,
|
||||||
|
onDelete: onAction.cascade,
|
||||||
|
onUpdate: onAction.cascade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roomOTAs = db.newTable("roomOTAs");
|
||||||
|
roomOTAs.addAttributes({
|
||||||
|
roomID: {
|
||||||
|
type: INT,
|
||||||
|
primaryKey: true,
|
||||||
|
foreginKey: {
|
||||||
|
link: rooms.roomID,
|
||||||
|
onDelete: onAction.cascade,
|
||||||
|
onUpdate: onAction.cascade
|
||||||
|
}
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
type: VARCHAR(128),
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
expires: { type: BIGINT },
|
||||||
|
usesLeft: { type: INT }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listCategories = db.newTable("listCategories");
|
||||||
|
listCategories.addAttributes({
|
||||||
|
listCatID: { type: INT, primaryKey: true, autoIncrement: true },
|
||||||
|
roomID: {
|
||||||
|
type: INT,
|
||||||
|
primaryKey: true,
|
||||||
|
foreginKey: {
|
||||||
|
link: rooms.roomID,
|
||||||
|
onDelete: onAction.cascade,
|
||||||
|
onUpdate: onAction.cascade
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: { type: TEXT },
|
||||||
|
weight: { type: INT },
|
||||||
|
color: { type: TEXT }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const articles = db.newTable("articles");
|
||||||
|
articles.addAttributes({
|
||||||
|
articleID: { type: INT, primaryKey: true, autoIncrement: true },
|
||||||
|
roomID: {
|
||||||
|
type: INT,
|
||||||
|
primaryKey: true,
|
||||||
|
foreginKey: {
|
||||||
|
link: rooms.roomID,
|
||||||
|
onDelete: onAction.cascade,
|
||||||
|
onUpdate: onAction.cascade
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state: { type: TINYINT, default: 0 },
|
||||||
|
title: { type: TEXT },
|
||||||
|
description: { type: TEXT },
|
||||||
|
|
||||||
|
category: {
|
||||||
|
type: INT,
|
||||||
|
foreginKey: {
|
||||||
|
link: listCategories.listCatID,
|
||||||
|
onDelete: onAction.setNull,
|
||||||
|
onUpdate: onAction.cascade
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unit: { type: SMALLINT },
|
||||||
|
amount: { type: TEXT },
|
||||||
|
|
||||||
|
//link: {type:INT} TODO: foreign key
|
||||||
|
|
||||||
|
});
|
41
src/sys/log.ts
Normal file
41
src/sys/log.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
export const debug = (name: string, ...args: any[]) => {
|
||||||
|
if(!global.debug)return;
|
||||||
|
console.log(
|
||||||
|
"\x1b[33m%s\x1b[0m"+
|
||||||
|
"\x1b[1m\x1b[32m%s\x1b[0m",
|
||||||
|
(new Date()).toLocaleString(),
|
||||||
|
` [${name}]:`,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const log = (name: string, ...args: string[]) => {
|
||||||
|
console.log(
|
||||||
|
"\x1b[33m%s\x1b[0m"+
|
||||||
|
"\x1b[1m\x1b[36m%s\x1b[0m",
|
||||||
|
(new Date()).toLocaleString(),
|
||||||
|
` [${name}]:`,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const warn = (name: string, ...args: any[]) => {
|
||||||
|
console.warn(
|
||||||
|
"\x1b[33m%s\x1b[0m"+
|
||||||
|
"\x1b[1m\x1b[36m%s\x1b[0m",
|
||||||
|
(new Date()).toLocaleString(),
|
||||||
|
` [${name}]:`,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const error = (name: string, ...args: any[]) => {
|
||||||
|
console.error(
|
||||||
|
"\x1b[33m%s\x1b[0m"+
|
||||||
|
"\x1b[1m\x1b[41m%s\x1b[0m\x1b[41m",
|
||||||
|
(new Date()).toLocaleString()+" ",
|
||||||
|
`[${name}]:`,
|
||||||
|
...args,
|
||||||
|
"\x1b[0m"
|
||||||
|
);
|
||||||
|
};
|
32
src/sys/settings.ts
Normal file
32
src/sys/settings.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { eq, exists, insert, not, select, update } from "dblang";
|
||||||
|
import { db, settings } from "./db.js";
|
||||||
|
|
||||||
|
export const SETTINGS = {
|
||||||
|
publicKey: "publicKey",
|
||||||
|
privateKey: "privateKey",
|
||||||
|
certExpires: "certExpires"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSettings = async (type: string) => {
|
||||||
|
let query = await select([settings.data], settings)
|
||||||
|
.where(eq(settings.type, type))
|
||||||
|
.query(db);
|
||||||
|
if (!query.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return query[0][settings.data];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setSettings = async (type: string, data: string) => {
|
||||||
|
try {
|
||||||
|
await insert(settings.type, settings.data)
|
||||||
|
.add(type, data)
|
||||||
|
.query(db);
|
||||||
|
} catch (error) {
|
||||||
|
await update(settings)
|
||||||
|
.set(settings.data, data)
|
||||||
|
.where(eq(settings.type, type))
|
||||||
|
.query(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
17
src/sys/tools.ts
Normal file
17
src/sys/tools.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
export const uts = ()=>{
|
||||||
|
return Math.floor(new Date().getTime()/1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key64 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
|
||||||
|
|
||||||
|
export const get64 = (l: number) => {
|
||||||
|
var out = "";
|
||||||
|
var val = crypto.webcrypto.getRandomValues(new Uint8Array(l));
|
||||||
|
|
||||||
|
for (var i = 0; i < l; i++) {
|
||||||
|
out += key64[val[i] % key64.length];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue