aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/ActionUtilities.ts26
-rw-r--r--src/server/ApiManagers/SearchManager.ts16
-rw-r--r--src/server/ApiManagers/SessionManager.ts58
-rw-r--r--src/server/DashSession.ts62
-rw-r--r--src/server/DashSession/DashSessionAgent.ts223
-rw-r--r--src/server/DashSession/templates/crash_instructions.txt14
-rw-r--r--src/server/DashSession/templates/remote_debug_instructions.txt16
-rw-r--r--src/server/IDatabase.ts24
-rw-r--r--src/server/MemoryDatabase.ts100
-rw-r--r--src/server/RouteManager.ts15
-rw-r--r--src/server/Search.ts8
-rw-r--r--src/server/Session/session.ts592
-rw-r--r--src/server/Session/session_config_schema.ts39
-rw-r--r--src/server/Websocket/Websocket.ts10
-rw-r--r--src/server/authentication/models/current_user_utils.ts2
-rw-r--r--src/server/database.ts19
-rw-r--r--src/server/index.ts45
-rw-r--r--src/server/repl.ts123
-rw-r--r--src/server/server_Initialization.ts4
19 files changed, 522 insertions, 874 deletions
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index a93566fb1..60f66c878 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -6,6 +6,7 @@ import * as rimraf from "rimraf";
import { yellow, Color } from 'colors';
import * as nodemailer from "nodemailer";
import { MailOptions } from "nodemailer/lib/json-transport";
+import Mail = require('nodemailer/lib/mailer');
const projectRoot = path.resolve(__dirname, "../../");
export function pathFromRoot(relative?: string) {
@@ -118,16 +119,24 @@ export namespace Email {
}
});
+ export interface DispatchOptions<T extends string | string[]> {
+ to: T;
+ subject: string;
+ content: string;
+ attachments?: Mail.Attachment | Mail.Attachment[];
+ }
+
export interface DispatchFailure {
recipient: string;
error: Error;
}
- export async function dispatchAll(recipients: string[], subject: string, content: string) {
+ export async function dispatchAll({ to, subject, content, attachments }: DispatchOptions<string[]>) {
const failures: DispatchFailure[] = [];
- await Promise.all(recipients.map(async (recipient: string) => {
+ await Promise.all(to.map(async recipient => {
let error: Error | null;
- if ((error = await Email.dispatch(recipient, subject, content)) !== null) {
+ const resolved = attachments ? "length" in attachments ? attachments : [attachments] : undefined;
+ if ((error = await Email.dispatch({ to: recipient, subject, content, attachments: resolved })) !== null) {
failures.push({
recipient,
error
@@ -137,16 +146,15 @@ export namespace Email {
return failures.length ? failures : undefined;
}
- export async function dispatch(recipient: string, subject: string, content: string): Promise<Error | null> {
+ export async function dispatch({ to, subject, content, attachments }: DispatchOptions<string>): Promise<Error | null> {
const mailOptions = {
- to: recipient,
+ to,
from: 'brownptcdash@gmail.com',
subject,
- text: `Hello ${recipient.split("@")[0]},\n\n${content}`
+ text: `Hello ${to.split("@")[0]},\n\n${content}`,
+ attachments
} as MailOptions;
- return new Promise<Error | null>(resolve => {
- smtpTransport.sendMail(mailOptions, resolve);
- });
+ return new Promise<Error | null>(resolve => smtpTransport.sendMail(mailOptions, resolve));
}
} \ No newline at end of file
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 316ba09ed..4ce12f9f3 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -8,6 +8,7 @@ import { red, cyan, yellow } from "colors";
import RouteSubscriber from "../RouteSubscriber";
import { exec } from "child_process";
import { onWindows } from "..";
+import { get } from "request-promise";
export class SearchManager extends ApiManager {
@@ -68,18 +69,25 @@ export class SearchManager extends ApiManager {
export namespace SolrManager {
+ const command = onWindows ? "solr.cmd" : "solr";
+
export async function SetRunning(status: boolean): Promise<boolean> {
const args = status ? "start" : "stop -p 8983";
- console.log(`Solr management: trying to ${args}`);
- exec(`${onWindows ? "solr.cmd" : "solr"} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => {
+ console.log(`solr management: trying to ${args}`);
+ exec(`${command} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => {
if (error) {
+ console.log(red(`solr management error: unable to ${args} server`));
console.log(red(error.message));
- console.log(red(`Solr management error: unable to ${args}`));
}
console.log(cyan(stdout));
console.log(yellow(stderr));
});
- return true;
+ try {
+ await get("http://localhost:8983");
+ return true;
+ } catch {
+ return false;
+ }
}
} \ No newline at end of file
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
new file mode 100644
index 000000000..f1629b8f0
--- /dev/null
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -0,0 +1,58 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied, AuthorizedCore, SecureHandler } from "../RouteManager";
+import RouteSubscriber from "../RouteSubscriber";
+import { sessionAgent } from "..";
+import { DashSessionAgent } from "../DashSession/DashSessionAgent";
+
+const permissionError = "You are not authorized!";
+
+export default class SessionManager extends ApiManager {
+
+ private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("session_key", ...params);
+
+ private authorizedAction = (handler: SecureHandler) => {
+ return (core: AuthorizedCore) => {
+ const { req: { params }, res, isRelease } = core;
+ if (!isRelease) {
+ return res.send("This can be run only on the release server.");
+ }
+ if (params.session_key !== process.env.session_key) {
+ return _permission_denied(res, permissionError);
+ }
+ return handler(core);
+ };
+ }
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: this.secureSubscriber("debug", "to?"),
+ secureHandler: this.authorizedAction(async ({ req: { params }, res }) => {
+ const to = params.to || DashSessionAgent.notificationRecipient;
+ const { error } = await sessionAgent.serverWorker.emit("debug", { to });
+ res.send(error ? error.message : `Your request was successful: the server captured and compressed (but did not save) a new back up. It was sent to ${to}.`);
+ })
+ });
+
+ register({
+ method: Method.GET,
+ subscription: this.secureSubscriber("backup"),
+ secureHandler: this.authorizedAction(async ({ res }) => {
+ const { error } = await sessionAgent.serverWorker.emit("backup");
+ res.send(error ? error.message : "Your request was successful: the server successfully created a new back up.");
+ })
+ });
+
+ register({
+ method: Method.GET,
+ subscription: this.secureSubscriber("kill"),
+ secureHandler: this.authorizedAction(({ res }) => {
+ res.send("Your request was successful: the server and its session have been killed.");
+ sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route");
+ })
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashSession.ts b/src/server/DashSession.ts
deleted file mode 100644
index 83ce7caaf..000000000
--- a/src/server/DashSession.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Session } from "./Session/session";
-import { Email } from "./ActionUtilities";
-import { red, yellow } from "colors";
-import { SolrManager } from "./ApiManagers/SearchManager";
-import { execSync } from "child_process";
-import { Utils } from "../Utils";
-import { WebSocket } from "./Websocket/Websocket";
-import { MessageStore } from "./Message";
-import { launchServer } from ".";
-
-/**
-* If we're the monitor (master) thread, we should launch the monitor logic for the session.
-* Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
-* our job should be to run the server.
-*/
-export class DashSessionAgent extends Session.AppliedSessionAgent {
-
- private readonly notificationRecipients = ["samuel_wilkins@brown.edu"];
- private readonly signature = "-Dash Server Session Manager";
-
- protected async launchMonitor() {
- const monitor = Session.Monitor.Create({
- key: async key => {
- // this sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
- // to kill the server via the /kill/:key route
- const content = `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}`;
- const failures = await Email.dispatchAll(this.notificationRecipients, "Server Termination Key", content);
- if (failures) {
- failures.map(({ recipient, error: { message } }) => monitor.log(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
- return false;
- }
- return true;
- },
- crash: async ({ name, message, stack }) => {
- const body = [
- "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:",
- `name:\n${name}`,
- `message:\n${message}`,
- `stack:\n${stack}`,
- "The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.",
- ].join("\n\n");
- const content = `${body}\n\n${this.signature}`;
- const failures = await Email.dispatchAll(this.notificationRecipients, "Dash Web Server Crash", content);
- if (failures) {
- failures.map(({ recipient, error: { message } }) => monitor.log(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
- return false;
- }
- return true;
- }
- });
- monitor.addReplCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] }));
- monitor.addReplCommand("solr", [/start|stop/], args => SolrManager.SetRunning(args[0] === "start"));
- return monitor;
- }
-
- protected async launchServerWorker() {
- const worker = Session.ServerWorker.Create(launchServer); // server initialization delegated to worker
- worker.addExitHandler(() => Utils.Emit(WebSocket._socket, MessageStore.ConnectionTerminated, "Manual"));
- return worker;
- }
-
-} \ No newline at end of file
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts
new file mode 100644
index 000000000..c55e01243
--- /dev/null
+++ b/src/server/DashSession/DashSessionAgent.ts
@@ -0,0 +1,223 @@
+import { Email, pathFromRoot } from "../ActionUtilities";
+import { red, yellow, green, cyan } from "colors";
+import { get } from "request-promise";
+import { Utils } from "../../Utils";
+import { WebSocket } from "../Websocket/Websocket";
+import { MessageStore } from "../Message";
+import { launchServer, onWindows } from "..";
+import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs";
+import * as Archiver from "archiver";
+import { resolve } from "path";
+import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session";
+import rimraf = require("rimraf");
+
+/**
+ * If we're the monitor (master) thread, we should launch the monitor logic for the session.
+ * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
+ * our job should be to run the server.
+ */
+export class DashSessionAgent extends AppliedSessionAgent {
+
+ private readonly signature = "-Dash Server Session Manager";
+ private readonly releaseDesktop = pathFromRoot("../../Desktop");
+
+ /**
+ * The core method invoked when the single master thread is initialized.
+ * Installs event hooks, repl commands and additional IPC listeners.
+ */
+ protected async initializeMonitor(monitor: Monitor, sessionKey: string): Promise<void> {
+ await this.dispatchSessionPassword(sessionKey);
+ monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
+ monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
+ monitor.addReplCommand("backup", [], this.backup);
+ monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to));
+ monitor.on("backup", this.backup);
+ monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to));
+ monitor.coreHooks.onCrashDetected(this.dispatchCrashReport);
+ }
+
+ /**
+ * The core method invoked when a server worker thread is initialized.
+ * Installs logic to be executed when the server worker dies.
+ */
+ protected async initializeServerWorker(): Promise<ServerWorker> {
+ const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
+ worker.addExitHandler(this.notifyClient);
+ return worker;
+ }
+
+ /**
+ * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally.
+ */
+ private _remoteDebugInstructions: string | undefined;
+ private generateDebugInstructions = (zipName: string, target: string): string => {
+ if (!this._remoteDebugInstructions) {
+ this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._remoteDebugInstructions
+ .replace(/__zipname__/, zipName)
+ .replace(/__target__/, target)
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * Prepares the body of the email with information regarding a crash event.
+ */
+ private _crashInstructions: string | undefined;
+ private generateCrashInstructions({ name, message, stack }: Error): string {
+ if (!this._crashInstructions) {
+ this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._crashInstructions
+ .replace(/__name__/, name || "[no error name found]")
+ .replace(/__message__/, message || "[no error message found]")
+ .replace(/__stack__/, stack || "[no error stack found]")
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
+ * to kill the server via the /kill/:key route.
+ */
+ private dispatchSessionPassword = async (sessionKey: string): Promise<void> => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ mainLog(green("dispatching session key..."));
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Release Session Admin Authentication Key",
+ content: [
+ `Here's the key for this session (started @ ${new Date().toUTCString()}):`,
+ sessionKey,
+ this.signature
+ ].join("\n\n")
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
+ mainLog(red("distribution of session key experienced errors"));
+ } else {
+ mainLog(green("successfully distributed session key to recipients"));
+ }
+ }
+
+ /**
+ * This sends an email with the generated crash report.
+ */
+ private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Web Server Crash",
+ content: this.generateCrashInstructions(crashCause)
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} ${yellow(`(${error.message})`)}`));
+ mainLog(red("distribution of crash notification experienced errors"));
+ } else {
+ mainLog(green("successfully distributed crash notification to recipients"));
+ }
+ }
+
+ /**
+ * Logic for interfacing with Solr. Either starts it,
+ * stops it, or rebuilds its indicies.
+ */
+ private executeSolrCommand = async (args: string[]): Promise<void> => {
+ const { exec, mainLog } = this.sessionMonitor;
+ const action = args[0];
+ if (action === "index") {
+ exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") });
+ } else {
+ const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`;
+ await exec(command, { cwd: "./solr-8.3.1/bin" });
+ try {
+ await get("http://localhost:8983");
+ mainLog(green("successfully connected to 8983 after running solr initialization"));
+ } catch {
+ mainLog(red("unable to connect at 8983 after running solr initialization"));
+ }
+ }
+ }
+
+ /**
+ * Broadcast to all clients that their connection
+ * is no longer valid, and explain why / what to expect.
+ */
+ private notifyClient: ExitHandler = reason => {
+ const { _socket } = WebSocket;
+ if (_socket) {
+ const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash";
+ Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
+ }
+ }
+
+ /**
+ * Performs a backup of the database, saved to the desktop subdirectory.
+ * This should work as is only on our specific release server.
+ */
+ private backup = async (): Promise<void> => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
+
+ /**
+ * Compress either a brand new backup or the most recent backup and send it
+ * as an attachment to an email, dispatched to the requested recipient.
+ * @param mode specifies whether or not to make a new backup before exporting
+ * @param to the recipient of the email
+ */
+ private async dispatchZippedDebugBackup(to: string): Promise<void> {
+ const { mainLog } = this.sessionMonitor;
+ try {
+ // if desired, complete an immediate backup to send
+ await this.backup();
+ mainLog("backup complete");
+
+ const backupsDirectory = `${this.releaseDesktop}/backups`;
+
+ // sort all backups by their modified time, and choose the most recent one
+ const target = readdirSync(backupsDirectory).map(filename => ({
+ modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
+ filename
+ })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
+ mainLog(`targeting ${target}...`);
+
+ // create a zip file and to it, write the contents of the backup directory
+ const zipName = `${target}.zip`;
+ const zipPath = `${this.releaseDesktop}/${zipName}`;
+ const targetPath = `${backupsDirectory}/${target}`;
+ const output = createWriteStream(zipPath);
+ const zip = Archiver('zip');
+ zip.pipe(output);
+ zip.directory(`${targetPath}/Dash`, false);
+ await zip.finalize();
+ mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
+
+ // dispatch the email to the recipient, containing the finalized zip file
+ const error = await Email.dispatch({
+ to,
+ subject: `Remote debug: compressed backup of ${target}...`,
+ content: this.generateDebugInstructions(zipName, target),
+ attachments: [{ filename: zipName, path: zipPath }]
+ });
+
+ // since this is intended to be a zero-footprint operation, clean up
+ // by unlinking both the backup generated earlier in the function and the compressed zip file.
+ // to generate a persistent backup, just run backup.
+ unlinkSync(zipPath);
+ rimraf.sync(targetPath);
+
+ // indicate success or failure
+ mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
+ error && mainLog(red(error.message));
+ } catch (error) {
+ mainLog(red("unable to dispatch zipped backup..."));
+ mainLog(red(error.message));
+ }
+ }
+
+}
+
+export namespace DashSessionAgent {
+
+ export const notificationRecipient = "brownptcdash@gmail.com";
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/templates/crash_instructions.txt b/src/server/DashSession/templates/crash_instructions.txt
new file mode 100644
index 000000000..65417919d
--- /dev/null
+++ b/src/server/DashSession/templates/crash_instructions.txt
@@ -0,0 +1,14 @@
+You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:
+
+name:
+__name__
+
+message:
+__message__
+
+stack:
+__stack__
+
+The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.
+
+__signature__ \ No newline at end of file
diff --git a/src/server/DashSession/templates/remote_debug_instructions.txt b/src/server/DashSession/templates/remote_debug_instructions.txt
new file mode 100644
index 000000000..c279c460a
--- /dev/null
+++ b/src/server/DashSession/templates/remote_debug_instructions.txt
@@ -0,0 +1,16 @@
+Instructions:
+
+Download this attachment, open your downloads folder and find this file (__zipname__).
+Right click on the zip file and select 'Extract to __target__\'.
+Open up the command line, and remember that you can get the path to any file or directory by literally dragging it from the file system and dropping it onto the terminal.
+Unless it's in your path, you'll want to navigate to the MongoDB bin directory, given for Windows:
+
+cd '/c/Program Files/MongoDB/Server/[your version, i.e. 4.0, goes here]/bin'
+
+Then run the following command (if you're in the bin folder, make that ./mongorestore ...):
+
+mongorestore --gzip [/path/to/directory/you/just/unzipped] --db Dash
+
+Assuming everything runs well, this will mirror your local database with that of the server. Now, just start the server locally and debug.
+
+__signature__ \ No newline at end of file
diff --git a/src/server/IDatabase.ts b/src/server/IDatabase.ts
new file mode 100644
index 000000000..6a63df485
--- /dev/null
+++ b/src/server/IDatabase.ts
@@ -0,0 +1,24 @@
+import * as mongodb from 'mongodb';
+import { Transferable } from './Message';
+
+export const DocumentsCollection = 'documents';
+export const NewDocumentsCollection = 'newDocuments';
+export interface IDatabase {
+ update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): Promise<void>;
+ updateMany(query: any, update: any, collectionName?: string): Promise<mongodb.WriteOpResult>;
+
+ replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): void;
+
+ delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+
+ deleteAll(collectionName?: string, persist?: boolean): Promise<any>;
+
+ insert(value: any, collectionName?: string): Promise<void>;
+
+ getDocument(id: string, fn: (result?: Transferable) => void, collectionName?: string): void;
+ getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName?: string): void;
+ visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName?: string): Promise<void>;
+
+ query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName?: string): Promise<mongodb.Cursor>;
+}
diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts
new file mode 100644
index 000000000..543f96e7f
--- /dev/null
+++ b/src/server/MemoryDatabase.ts
@@ -0,0 +1,100 @@
+import { IDatabase, DocumentsCollection, NewDocumentsCollection } from './IDatabase';
+import { Transferable } from './Message';
+import * as mongodb from 'mongodb';
+
+export class MemoryDatabase implements IDatabase {
+
+ private db: { [collectionName: string]: { [id: string]: any } } = {};
+
+ private getCollection(collectionName: string) {
+ const collection = this.db[collectionName];
+ if (collection) {
+ return collection;
+ } else {
+ return this.db[collectionName] = {};
+ }
+ }
+
+ public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise<void> {
+ const collection = this.getCollection(collectionName);
+ const set = "$set";
+ if (set in value) {
+ let currentVal = collection[id] ?? (collection[id] = {});
+ const val = value[set];
+ for (const key in val) {
+ const keys = key.split(".");
+ for (let i = 0; i < keys.length - 1; i++) {
+ const k = keys[i];
+ if (typeof currentVal[k] === "object") {
+ currentVal = currentVal[k];
+ } else {
+ currentVal[k] = {};
+ currentVal = currentVal[k];
+ }
+ }
+ currentVal[keys[keys.length - 1]] = val[key];
+ }
+ } else {
+ collection[id] = value;
+ }
+ callback(null as any, {} as any);
+ return Promise.resolve(undefined);
+ }
+
+ public updateMany(query: any, update: any, collectionName = NewDocumentsCollection): Promise<mongodb.WriteOpResult> {
+ throw new Error("Can't updateMany a MemoryDatabase");
+ }
+
+ public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName = DocumentsCollection): void {
+ this.update(id, value, callback, upsert, collectionName);
+ }
+
+ public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: any, collectionName = DocumentsCollection): Promise<mongodb.DeleteWriteOpResultObject> {
+ const i = id.id ?? id;
+ delete this.getCollection(collectionName)[i];
+
+ return Promise.resolve({} as any);
+ }
+
+ public deleteAll(collectionName = DocumentsCollection, _persist = true): Promise<any> {
+ delete this.db[collectionName];
+ return Promise.resolve();
+ }
+
+ public insert(value: any, collectionName = DocumentsCollection): Promise<void> {
+ const id = value.id;
+ this.getCollection(collectionName)[id] = value;
+ return Promise.resolve();
+ }
+
+ public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = NewDocumentsCollection): void {
+ fn(this.getCollection(collectionName)[id]);
+ }
+ public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = DocumentsCollection): void {
+ fn(ids.map(id => this.getCollection(collectionName)[id]));
+ }
+
+ public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = NewDocumentsCollection): Promise<void> {
+ const visited = new Set<string>();
+ while (ids.length) {
+ const count = Math.min(ids.length, 1000);
+ const index = ids.length - count;
+ const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
+ if (!fetchIds.length) {
+ continue;
+ }
+ const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName));
+ for (const doc of docs) {
+ const id = doc.id;
+ visited.add(id);
+ ids.push(...(await fn(doc)));
+ }
+ }
+ }
+
+ public query(): Promise<mongodb.Cursor> {
+ throw new Error("Can't query a MemoryDatabase");
+ }
+}
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 25259bd88..b07aef74d 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -14,7 +14,8 @@ export interface CoreArguments {
isRelease: boolean;
}
-export type SecureHandler = (core: CoreArguments & { user: DashUserModel }) => any | Promise<any>;
+export type AuthorizedCore = CoreArguments & { user: DashUserModel };
+export type SecureHandler = (core: AuthorizedCore) => any | Promise<any>;
export type PublicHandler = (core: CoreArguments) => any | Promise<any>;
export type ErrorHandler = (core: CoreArguments & { error: any }) => any | Promise<any>;
@@ -67,7 +68,7 @@ export default class RouteManager {
console.log('please remove all duplicate routes before continuing');
}
if (malformedCount) {
- console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$`);
+ console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$`);
}
process.exit(1);
} else {
@@ -85,7 +86,11 @@ export default class RouteManager {
const { method, subscription, secureHandler: onValidation, publicHandler: onUnauthenticated, errorHandler: onError } = initializer;
const isRelease = this._isRelease;
const supervised = async (req: express.Request, res: express.Response) => {
- const { user, originalUrl: target } = req;
+ let { user } = req;
+ const { originalUrl: target } = req;
+ if (process.env.DB === "MEM" && !user) {
+ user = { id: "guest", email: "", userDocumentId: "guestDocId" };
+ }
const core = { req, res, isRelease };
const tryExecute = async (toExecute: (args: any) => any | Promise<any>, args: any) => {
try {
@@ -127,7 +132,7 @@ export default class RouteManager {
} else {
route = subscriber.build;
}
- if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$/g.test(route)) {
+ if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$/g.test(route)) {
this.failedRegistrations.push({
reason: RegistrationError.Malformed,
route
@@ -192,5 +197,5 @@ export function _permission_denied(res: express.Response, message?: string) {
if (message) {
res.statusMessage = message;
}
- res.status(STATUS.BAD_REQUEST).send("Permission Denied!");
+ res.status(STATUS.PERMISSION_DENIED).send("Permission Denied!");
}
diff --git a/src/server/Search.ts b/src/server/Search.ts
index 2b59c14b1..21064e520 100644
--- a/src/server/Search.ts
+++ b/src/server/Search.ts
@@ -1,4 +1,5 @@
import * as rp from 'request-promise';
+import { red } from 'colors';
const pathTo = (relative: string) => `http://localhost:8983/solr/dash/${relative}`;
@@ -43,7 +44,7 @@ export namespace Search {
export async function clear() {
try {
- return rp.post(pathTo("update"), {
+ await rp.post(pathTo("update"), {
body: {
delete: {
query: "*:*"
@@ -51,7 +52,10 @@ export namespace Search {
},
json: true
});
- } catch { }
+ } catch (e) {
+ console.log(red("Unable to clear search..."));
+ console.log(red(e.message));
+ }
}
export async function deleteDocuments(docs: string[]) {
diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts
deleted file mode 100644
index 06a076ae4..000000000
--- a/src/server/Session/session.ts
+++ /dev/null
@@ -1,592 +0,0 @@
-import { red, cyan, green, yellow, magenta, blue, white } from "colors";
-import { on, fork, setupMaster, Worker, isMaster, isWorker } from "cluster";
-import { get } from "request-promise";
-import { Utils } from "../../Utils";
-import Repl, { ReplAction } from "../repl";
-import { readFileSync } from "fs";
-import { validate, ValidationError } from "jsonschema";
-import { configurationSchema } from "./session_config_schema";
-
-/**
- * This namespace relies on NodeJS's cluster module, which allows a parent (master) process to share
- * code with its children (workers). A simple `isMaster` flag indicates who is trying to access
- * the code, and thus determines the functionality that actually gets invoked (checked by the caller, not internally).
- *
- * Think of the master thread as a factory, and the workers as the helpers that actually run the server.
- *
- * So, when we run `npm start`, given the appropriate check, initializeMaster() is called in the parent process
- * This will spawn off its own child process (by default, mirrors the execution path of its parent),
- * in which initializeWorker() is invoked.
- */
-export namespace Session {
-
- export abstract class AppliedSessionAgent {
-
- // the following two methods allow the developer to create a custom
- // session and use the built in customization options for each thread
- protected abstract async launchMonitor(): Promise<Session.Monitor>;
- protected abstract async launchServerWorker(): Promise<Session.ServerWorker>;
-
- private launched = false;
-
- public killSession = (reason: string, graceful = true, errorCode = 0) => {
- const target = isMaster ? this.sessionMonitor : this.serverWorker;
- target.killSession(reason, graceful, errorCode);
- }
-
- private sessionMonitorRef: Session.Monitor | undefined;
- public get sessionMonitor(): Session.Monitor {
- if (!isMaster) {
- this.serverWorker.sendMonitorAction("kill", {
- graceful: false,
- reason: "Cannot access the session monitor directly from the server worker thread.",
- errorCode: 1
- });
- throw new Error();
- }
- return this.sessionMonitorRef!;
- }
-
- private serverWorkerRef: Session.ServerWorker | undefined;
- public get serverWorker(): Session.ServerWorker {
- if (isMaster) {
- throw new Error("Cannot access the server worker directly from the session monitor thread");
- }
- return this.serverWorkerRef!;
- }
-
- public async launch(): Promise<void> {
- if (!this.launched) {
- this.launched = true;
- if (isMaster) {
- this.sessionMonitorRef = await this.launchMonitor();
- } else {
- this.serverWorkerRef = await this.launchServerWorker();
- }
- } else {
- throw new Error("Cannot launch a session thread more than once per process.");
- }
- }
-
- }
-
- interface Configuration {
- showServerOutput: boolean;
- masterIdentifier: string;
- workerIdentifier: string;
- ports: { [description: string]: number };
- pollingRoute: string;
- pollingIntervalSeconds: number;
- pollingFailureTolerance: number;
- [key: string]: any;
- }
-
- const defaultConfiguration: Configuration = {
- showServerOutput: false,
- masterIdentifier: yellow("__monitor__:"),
- workerIdentifier: magenta("__server__:"),
- ports: { server: 3000 },
- pollingRoute: "/",
- pollingIntervalSeconds: 30,
- pollingFailureTolerance: 0
- };
-
- export type ExitHandler = (reason: Error | null) => void | Promise<void>;
-
- export namespace Monitor {
-
- export interface NotifierHooks {
- key?: (key: string) => (boolean | Promise<boolean>);
- crash?: (error: Error) => (boolean | Promise<boolean>);
- }
-
- export interface Action {
- message: string;
- args: any;
- }
-
- export type ServerMessageHandler = (action: Action) => void | Promise<void>;
-
- }
-
- /**
- * Validates and reads the configuration file, accordingly builds a child process factory
- * and spawns off an initial process that will respawn as predecessors die.
- */
- export class Monitor {
-
- private static count = 0;
- private exitHandlers: ExitHandler[] = [];
- private readonly notifiers: Monitor.NotifierHooks | undefined;
- private readonly configuration: Configuration;
- private onMessage: { [message: string]: Monitor.ServerMessageHandler[] | undefined } = {};
- private activeWorker: Worker | undefined;
- private key: string | undefined;
- private repl: Repl;
-
- public static Create(notifiers?: Monitor.NotifierHooks) {
- if (isWorker) {
- process.send?.({
- action: {
- message: "kill",
- args: {
- reason: "cannot create a monitor on the worker process.",
- graceful: false,
- errorCode: 1
- }
- }
- });
- process.exit(1);
- } else if (++Monitor.count > 1) {
- console.error(red("cannot create more than one monitor."));
- process.exit(1);
- } else {
- return new Monitor(notifiers);
- }
- }
-
- /**
- * Kill this session and its active child
- * server process, either gracefully (may wait
- * indefinitely, but at least allows active networking
- * requests to complete) or immediately.
- */
- public killSession = async (reason: string, graceful = true, errorCode = 0) => {
- this.log(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`));
- this.log(`reason: ${(red(reason))}`);
- await this.executeExitHandlers(null);
- this.tryKillActiveWorker(graceful);
- process.exit(errorCode);
- }
-
- /**
- * Execute the list of functions registered to be called
- * whenever the process exits.
- */
- public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
- /**
- * Extend the default repl by adding in custom commands
- * that can invoke application logic external to this module
- */
- public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
- this.repl.registerCommand(basename, argPatterns, action);
- }
-
- /**
- * Add a listener at this message. When the monitor process
- * receives a message, it will invoke all registered functions.
- */
- public addServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
- const handlers = this.onMessage[message];
- if (handlers) {
- handlers.push(handler);
- } else {
- this.onMessage[message] = [handler];
- }
- }
-
- /**
- * Unregister a given listener at this message.
- */
- public removeServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
- const handlers = this.onMessage[message];
- if (handlers) {
- const index = handlers.indexOf(handler);
- if (index > -1) {
- handlers.splice(index, 1);
- }
- }
- }
-
- /**
- * Unregister all listeners at this message.
- */
- public clearServerMessageListeners = (message: string) => this.onMessage[message] = undefined;
-
- private constructor(notifiers?: Monitor.NotifierHooks) {
- this.notifiers = notifiers;
-
- console.log(this.timestamp(), cyan("initializing session..."));
-
- this.configuration = this.loadAndValidateConfiguration();
- this.initializeSessionKey();
- // determines whether or not we see the compilation / initialization / runtime output of each child server process
- setupMaster({ silent: !this.configuration.showServerOutput });
-
- // handle exceptions in the master thread - there shouldn't be many of these
- // the IPC (inter process communication) channel closed exception can't seem
- // to be caught in a try catch, and is inconsequential, so it is ignored
- process.on("uncaughtException", ({ message, stack }): void => {
- if (message !== "Channel closed") {
- this.log(red(message));
- if (stack) {
- this.log(`uncaught exception\n${red(stack)}`);
- }
- }
- });
-
- // a helpful cluster event called on the master thread each time a child process exits
- on("exit", ({ process: { pid } }, code, signal) => {
- const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`;
- this.log(cyan(prompt));
- // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one
- this.spawn();
- });
-
- this.repl = this.initializeRepl();
- this.spawn();
- }
-
-
- /**
- * Generates a blue UTC string associated with the time
- * of invocation.
- */
- private timestamp = () => blue(`[${new Date().toUTCString()}]`);
-
- /**
- * A formatted, identified and timestamped log in color
- */
- public log = (...optionalParams: any[]) => {
- console.log(this.timestamp(), this.configuration.masterIdentifier, ...optionalParams);
- }
-
- /**
- * If the caller has indicated an interest
- * in being notified of this feature, creates
- * a GUID for this session that can, for example,
- * be used as authentication for killing the server
- * (checked externally).
- */
- private initializeSessionKey = async (): Promise<void> => {
- if (this.notifiers?.key) {
- this.key = Utils.GenerateGuid();
- const success = await this.notifiers.key(this.key);
- const statement = success ? green("distributed session key to recipients") : red("distribution of session key failed");
- this.log(statement);
- }
- }
-
- /**
- * Builds the repl that allows the following commands to be typed into stdin of the master thread.
- */
- private initializeRepl = (): Repl => {
- const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.configuration.masterIdentifier}` });
- const boolean = /true|false/;
- const number = /\d+/;
- const letters = /[a-zA-Z]+/;
- repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0));
- repl.registerCommand("restart", [/clean|force/], args => this.tryKillActiveWorker(args[0] === "clean"));
- repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true"));
- repl.registerCommand("set", [/polling/, number, boolean], args => {
- const newPollingIntervalSeconds = Math.floor(Number(args[2]));
- if (newPollingIntervalSeconds < 0) {
- this.log(red("the polling interval must be a non-negative integer"));
- } else {
- if (newPollingIntervalSeconds !== this.configuration.pollingIntervalSeconds) {
- this.configuration.pollingIntervalSeconds = newPollingIntervalSeconds;
- if (args[3] === "true") {
- this.activeWorker?.send({ newPollingIntervalSeconds });
- }
- }
- }
- });
- return repl;
- }
-
- /**
- * Reads in configuration .json file only once, in the master thread
- * and pass down any variables the pertinent to the child processes as environment variables.
- */
- private loadAndValidateConfiguration = (): Configuration => {
- try {
- console.log(this.timestamp(), cyan("validating configuration..."));
- const configuration: Configuration = JSON.parse(readFileSync('./session.config.json', 'utf8'));
- const options = {
- throwError: true,
- allowUnknownAttributes: false
- };
- // ensure all necessary and no excess information is specified by the configuration file
- validate(configuration, configurationSchema, options);
- let formatMaster = true;
- let formatWorker = true;
- Object.keys(defaultConfiguration).forEach(property => {
- if (!configuration[property]) {
- if (property === "masterIdentifier") {
- formatMaster = false;
- } else if (property === "workerIdentifier") {
- formatWorker = false;
- }
- configuration[property] = defaultConfiguration[property];
- }
- });
- if (formatMaster) {
- configuration.masterIdentifier = yellow(configuration.masterIdentifier + ":");
- }
- if (formatWorker) {
- configuration.workerIdentifier = magenta(configuration.workerIdentifier + ":");
- }
- return configuration;
- } catch (error) {
- if (error instanceof ValidationError) {
- console.log(red("\nSession configuration failed."));
- console.log("The given session.config.json configuration file is invalid.");
- console.log(`${error.instance}: ${error.stack}`);
- process.exit(0);
- } else if (error.code === "ENOENT" && error.path === "./session.config.json") {
- console.log(cyan("Loading default session parameters..."));
- console.log("Consider including a session.config.json configuration file in your project root for customization.");
- return defaultConfiguration;
- } else {
- console.log(red("\nSession configuration failed."));
- console.log("The following unknown error occurred during configuration.");
- console.log(error.stack);
- process.exit(0);
- }
- }
- }
-
-
- private executeExitHandlers = async (reason: Error | null) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
- /**
- * Attempts to kill the active worker gracefully, unless otherwise specified.
- */
- private tryKillActiveWorker = (graceful = true): boolean => {
- if (!this.activeWorker?.isDead()) {
- if (graceful) {
- this.activeWorker?.send({ manualExit: true });
- } else {
- this.activeWorker?.process.kill();
- }
- return true;
- }
- return false;
- }
-
- /**
- * Allows the caller to set the port at which the target (be it the server,
- * the websocket, some other custom port) is listening. If an immediate restart
- * is specified, this monitor will kill the active child and re-launch the server
- * at the port. Otherwise, the updated port won't be used until / unless the child
- * dies on its own and triggers a restart.
- */
- private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => {
- if (value > 1023 && value < 65536) {
- this.configuration.ports[port] = value;
- if (immediateRestart) {
- this.tryKillActiveWorker();
- }
- } else {
- this.log(red(`${port} is an invalid port number`));
- }
- }
-
- /**
- * Kills the current active worker and proceeds to spawn a new worker,
- * feeding in configuration information as environment variables.
- */
- private spawn = (): void => {
- const {
- pollingRoute,
- pollingFailureTolerance,
- pollingIntervalSeconds,
- ports
- } = this.configuration;
- this.tryKillActiveWorker();
- this.activeWorker = fork({
- pollingRoute,
- pollingFailureTolerance,
- serverPort: ports.server,
- socketPort: ports.socket,
- pollingIntervalSeconds,
- session_key: this.key
- });
- this.log(cyan(`spawned new server worker with process id ${this.activeWorker.process.pid}`));
- // an IPC message handler that executes actions on the master thread when prompted by the active worker
- this.activeWorker.on("message", async ({ lifecycle, action }) => {
- if (action) {
- const { message, args } = action as Monitor.Action;
- console.log(this.timestamp(), `${this.configuration.workerIdentifier} action requested (${cyan(message)})`);
- switch (message) {
- case "kill":
- const { reason, graceful, errorCode } = args;
- this.killSession(reason, graceful, errorCode);
- break;
- case "notify_crash":
- if (this.notifiers?.crash) {
- const { error } = args;
- const success = await this.notifiers.crash(error);
- const statement = success ? green("distributed crash notification to recipients") : red("distribution of crash notification failed");
- this.log(statement);
- }
- break;
- case "set_port":
- const { port, value, immediateRestart } = args;
- this.setPort(port, value, immediateRestart);
- break;
- }
- const handlers = this.onMessage[message];
- if (handlers) {
- handlers.forEach(handler => handler({ message, args }));
- }
- } else if (lifecycle) {
- console.log(this.timestamp(), `${this.configuration.workerIdentifier} lifecycle phase (${lifecycle})`);
- }
- });
- }
-
- }
-
- /**
- * Effectively, each worker repairs the connection to the server by reintroducing a consistent state
- * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification
- * email if the server encounters an uncaught exception or if the server cannot be reached.
- */
- export class ServerWorker {
-
- private static count = 0;
- private shouldServerBeResponsive = false;
- private exitHandlers: ExitHandler[] = [];
- private pollingFailureCount = 0;
- private pollingIntervalSeconds: number;
- private pollingFailureTolerance: number;
- private pollTarget: string;
- private serverPort: number;
-
- public static Create(work: Function) {
- if (isMaster) {
- console.error(red("cannot create a worker on the monitor process."));
- process.exit(1);
- } else if (++ServerWorker.count > 1) {
- process.send?.({
- action: {
- message: "kill", args: {
- reason: "cannot create more than one worker on a given worker process.",
- graceful: false,
- errorCode: 1
- }
- }
- });
- process.exit(1);
- } else {
- return new ServerWorker(work);
- }
- }
-
- /**
- * Allows developers to invoke application specific logic
- * by hooking into the exiting of the server process.
- */
- public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
- /**
- * Kill the session monitor (parent process) from this
- * server worker (child process). This will also kill
- * this process (child process).
- */
- public killSession = (reason: string, graceful = true, errorCode = 0) => this.sendMonitorAction("kill", { reason, graceful, errorCode });
-
- /**
- * A convenience wrapper to tell the session monitor (parent process)
- * to carry out the action with the specified message and arguments.
- */
- public sendMonitorAction = (message: string, args?: any) => process.send!({ action: { message, args } });
-
- private constructor(work: Function) {
- this.lifecycleNotification(green(`initializing process... (${white(`${process.execPath} ${process.execArgv.join(" ")}`)})`));
-
- const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env;
- this.serverPort = Number(serverPort);
- this.pollingIntervalSeconds = Number(pollingIntervalSeconds);
- this.pollingFailureTolerance = Number(pollingFailureTolerance);
- this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`;
-
- this.configureProcess();
- work();
- this.pollServer();
- }
-
- /**
- * Set up message and uncaught exception handlers for this
- * server process.
- */
- private configureProcess = () => {
- // updates the local values of variables to the those sent from master
- process.on("message", async ({ newPollingIntervalSeconds, manualExit }) => {
- if (newPollingIntervalSeconds !== undefined) {
- this.pollingIntervalSeconds = newPollingIntervalSeconds;
- }
- if (manualExit !== undefined) {
- await this.executeExitHandlers(null);
- process.exit(0);
- }
- });
-
- // one reason to exit, as the process might be in an inconsistent state after such an exception
- process.on('uncaughtException', this.proactiveUnplannedExit);
- }
-
- /**
- * Execute the list of functions registered to be called
- * whenever the process exits.
- */
- private executeExitHandlers = async (reason: Error | null) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
- /**
- * Notify master thread (which will log update in the console) of initialization via IPC.
- */
- public lifecycleNotification = (event: string) => process.send?.({ lifecycle: event });
-
- /**
- * Called whenever the process has a reason to terminate, either through an uncaught exception
- * in the process (potentially inconsistent state) or the server cannot be reached.
- */
- private proactiveUnplannedExit = async (error: Error): Promise<void> => {
- this.shouldServerBeResponsive = false;
- // communicates via IPC to the master thread that it should dispatch a crash notification email
- this.sendMonitorAction("notify_crash", { error });
- await this.executeExitHandlers(error);
- // notify master thread (which will log update in the console) of crash event via IPC
- this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`));
- this.lifecycleNotification(red(error.message));
- process.exit(1);
- }
-
- /**
- * This monitors the health of the server by submitting a get request to whatever port / route specified
- * by the configuration every n seconds, where n is also given by the configuration.
- */
- private pollServer = async (): Promise<void> => {
- await new Promise<void>(resolve => {
- setTimeout(async () => {
- try {
- await get(this.pollTarget);
- if (!this.shouldServerBeResponsive) {
- // notify monitor thread that the server is up and running
- this.lifecycleNotification(green(`listening on ${this.serverPort}...`));
- }
- this.shouldServerBeResponsive = true;
- resolve();
- } catch (error) {
- // if we expect the server to be unavailable, i.e. during compilation,
- // the listening variable is false, activeExit will return early and the child
- // process will continue
- if (this.shouldServerBeResponsive) {
- if (++this.pollingFailureCount > this.pollingFailureTolerance) {
- this.proactiveUnplannedExit(error);
- } else {
- this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`));
- }
- }
- }
- }, 1000 * this.pollingIntervalSeconds);
- });
- // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed
- this.pollServer();
- }
-
- }
-
-} \ No newline at end of file
diff --git a/src/server/Session/session_config_schema.ts b/src/server/Session/session_config_schema.ts
deleted file mode 100644
index 5a85a45e3..000000000
--- a/src/server/Session/session_config_schema.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Schema } from "jsonschema";
-
-export const configurationSchema: Schema = {
- id: "/configuration",
- type: "object",
- properties: {
- ports: {
- type: "object",
- properties: {
- server: { type: "number", minimum: 1024, maximum: 65535 },
- socket: { type: "number", minimum: 1024, maximum: 65535 }
- },
- required: ["server"],
- additionalProperties: true
- },
- pollingRoute: {
- type: "string",
- pattern: /\/[a-zA-Z]*/g
- },
- masterIdentifier: {
- type: "string",
- minLength: 1
- },
- workerIdentifier: {
- type: "string",
- minLength: 1
- },
- showServerOutput: { type: "boolean" },
- pollingIntervalSeconds: {
- type: "number",
- minimum: 1,
- maximum: 86400
- },
- pollingFailureTolerance: {
- type: "number",
- minimum: 0,
- }
- }
-}; \ No newline at end of file
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index 578147d60..6dda6956e 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -28,7 +28,7 @@ export namespace WebSocket {
function initialize(isRelease: boolean) {
const endpoint = io();
- endpoint.on("connection", function (socket: Socket) {
+ endpoint.on("connection", function(socket: Socket) {
_socket = socket;
socket.use((_packet, next) => {
@@ -83,7 +83,9 @@ export namespace WebSocket {
export async function deleteFields() {
await Database.Instance.deleteAll();
- await Search.clear();
+ if (process.env.DISABLE_SEARCH !== "true") {
+ await Search.clear();
+ }
await Database.Instance.deleteAll('newDocuments');
}
@@ -92,7 +94,9 @@ export namespace WebSocket {
await Database.Instance.deleteAll('newDocuments');
await Database.Instance.deleteAll('sessions');
await Database.Instance.deleteAll('users');
- await Search.clear();
+ if (process.env.DISABLE_SEARCH !== "true") {
+ await Search.clear();
+ }
}
function barReceived(socket: SocketIO.Socket, userEmail: string) {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index eb48b28f2..4a12c1ee9 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -372,4 +372,4 @@ export class CurrentUserUtils {
};
return recurs([] as Attribute[], schema ? schema.rootAttributeGroup : undefined);
}
-} \ No newline at end of file
+}
diff --git a/src/server/database.ts b/src/server/database.ts
index 6e0771c11..83ce865c6 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -5,6 +5,8 @@ import { Utils, emptyFunction } from '../Utils';
import { DashUploadUtils } from './DashUploadUtils';
import { Credentials } from 'google-auth-library';
import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils';
+import { IDatabase } from './IDatabase';
+import { MemoryDatabase } from './MemoryDatabase';
import * as mongoose from 'mongoose';
export namespace Database {
@@ -44,7 +46,7 @@ export namespace Database {
}
}
- class Database {
+ class Database implements IDatabase {
public static DocumentsCollection = 'documents';
private MongoClient = mongodb.MongoClient;
private currentWrites: { [id: string]: Promise<void> } = {};
@@ -215,7 +217,7 @@ export namespace Database {
if (!fetchIds.length) {
continue;
}
- const docs = await new Promise<{ [key: string]: any }[]>(res => Instance.getDocuments(fetchIds, res, "newDocuments"));
+ const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName));
for (const doc of docs) {
const id = doc.id;
visited.add(id);
@@ -262,7 +264,16 @@ export namespace Database {
}
}
- export const Instance = new Database();
+ function getDatabase() {
+ switch (process.env.DB) {
+ case "MEM":
+ return new MemoryDatabase();
+ default:
+ return new Database();
+ }
+ }
+
+ export const Instance: IDatabase = getDatabase();
export namespace Auxiliary {
@@ -331,4 +342,4 @@ export namespace Database {
}
-} \ No newline at end of file
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index 6b3dfd614..313a2f0e2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5,11 +5,11 @@ import * as path from 'path';
import { Database } from './database';
import { DashUploadUtils } from './DashUploadUtils';
import RouteSubscriber from './RouteSubscriber';
-import initializeServer from './server_initialization';
+import initializeServer from './server_Initialization';
import RouteManager, { Method, _success, _permission_denied, _error, _invalid, PublicHandler } from './RouteManager';
import * as qs from 'query-string';
import UtilManager from './ApiManagers/UtilManager';
-import { SearchManager, SolrManager } from './ApiManagers/SearchManager';
+import { SearchManager } from './ApiManagers/SearchManager';
import UserManager from './ApiManagers/UserManager';
import { WebSocket } from './Websocket/Websocket';
import DownloadManager from './ApiManagers/DownloadManager';
@@ -17,16 +17,17 @@ import { GoogleCredentialsLoader } from './credentials/CredentialsLoader';
import DeleteManager from "./ApiManagers/DeleteManager";
import PDFManager from "./ApiManagers/PDFManager";
import UploadManager from "./ApiManagers/UploadManager";
-import { log_execution, Email } from "./ActionUtilities";
+import { log_execution } from "./ActionUtilities";
import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
import { Logger } from "./ProcessFactory";
-import { yellow, red } from "colors";
-import { Session } from "./Session/session";
-import { DashSessionAgent } from "./DashSession";
+import { yellow } from "colors";
+import { DashSessionAgent } from "./DashSession/DashSessionAgent";
+import SessionManager from "./ApiManagers/SessionManager";
+import { AppliedSessionAgent } from "resilient-server-session";
export const onWindows = process.platform === "win32";
-export let sessionAgent: Session.AppliedSessionAgent;
+export let sessionAgent: AppliedSessionAgent;
export const publicDirectory = path.resolve(__dirname, "public");
export const filesDirectory = path.resolve(publicDirectory, "files");
@@ -40,11 +41,13 @@ async function preliminaryFunctions() {
await GoogleCredentialsLoader.loadCredentials();
GoogleApiServerUtils.processProjectCredentials();
await DashUploadUtils.buildFileDirectories();
- await log_execution({
- startMessage: "attempting to initialize mongodb connection",
- endMessage: "connection outcome determined",
- action: Database.tryInitializeConnection
- });
+ if (process.env.DB !== "MEM") {
+ await log_execution({
+ startMessage: "attempting to initialize mongodb connection",
+ endMessage: "connection outcome determined",
+ action: Database.tryInitializeConnection
+ });
+ }
}
/**
@@ -58,6 +61,7 @@ async function preliminaryFunctions() {
*/
function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) {
const managers = [
+ new SessionManager(),
new UserManager(),
new UploadManager(),
new DownloadManager(),
@@ -88,21 +92,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
secureHandler: ({ res }) => res.send(true)
});
- addSupervisedRoute({
- method: Method.GET,
- subscription: new RouteSubscriber("kill").add("key"),
- secureHandler: ({ req, res }) => {
- if (req.params.key === process.env.session_key) {
- res.send("<img src='https://media.giphy.com/media/NGIfqtcS81qi4/giphy.gif' style='width:100%;height:100%;'/>");
- setTimeout(() => {
- sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route", false);
- }, 5000);
- } else {
- res.redirect("/home");
- }
- }
- });
-
const serve: PublicHandler = ({ req, res }) => {
const detector = new mobileDetect(req.headers['user-agent'] || "");
const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -155,4 +144,4 @@ if (process.env.RELEASE) {
(sessionAgent = new DashSessionAgent()).launch();
} else {
launchServer();
-} \ No newline at end of file
+}
diff --git a/src/server/repl.ts b/src/server/repl.ts
deleted file mode 100644
index c4526528e..000000000
--- a/src/server/repl.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { createInterface, Interface } from "readline";
-import { red, green, white } from "colors";
-
-export interface Configuration {
- identifier: () => string | string;
- onInvalid?: (command: string, validCommand: boolean) => string | string;
- onValid?: (success?: string) => string | string;
- isCaseSensitive?: boolean;
-}
-
-export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>;
-export interface Registration {
- argPatterns: RegExp[];
- action: ReplAction;
-}
-
-export default class Repl {
- private identifier: () => string | string;
- private onInvalid: ((command: string, validCommand: boolean) => string) | string;
- private onValid: ((success: string) => string) | string;
- private isCaseSensitive: boolean;
- private commandMap = new Map<string, Registration[]>();
- public interface: Interface;
- private busy = false;
- private keys: string | undefined;
-
- constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) {
- this.identifier = prompt;
- this.onInvalid = onInvalid || this.usage;
- this.onValid = onValid || this.success;
- this.isCaseSensitive = isCaseSensitive ?? true;
- this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
- }
-
- private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier();
-
- private usage = (command: string, validCommand: boolean) => {
- if (validCommand) {
- const formatted = white(command);
- const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n'));
- return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`;
- } else {
- const resolved = this.keys;
- if (resolved) {
- return resolved;
- }
- const members: string[] = [];
- const keys = this.commandMap.keys();
- let next: IteratorResult<string>;
- while (!(next = keys.next()).done) {
- members.push(next.value);
- }
- return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`;
- }
- }
-
- private success = (command: string) => `${this.resolvedIdentifier()} completed execution of ${white(command)}`;
-
- public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
- const existing = this.commandMap.get(basename);
- const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input));
- const registration = { argPatterns: converted, action };
- if (existing) {
- existing.push(registration);
- } else {
- this.commandMap.set(basename, [registration]);
- }
- }
-
- private invalid = (command: string, validCommand: boolean) => {
- console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand)));
- this.busy = false;
- }
-
- private valid = (command: string) => {
- console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command)));
- this.busy = false;
- }
-
- private considerInput = async (line: string) => {
- if (this.busy) {
- console.log(red("Busy"));
- return;
- }
- this.busy = true;
- line = line.trim();
- if (this.isCaseSensitive) {
- line = line.toLowerCase();
- }
- const [command, ...args] = line.split(/\s+/g);
- if (!command) {
- return this.invalid(command, false);
- }
- const registered = this.commandMap.get(command);
- if (registered) {
- const { length } = args;
- const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length);
- for (const { argPatterns, action } of candidates) {
- const parsed: string[] = [];
- let matched = true;
- if (length) {
- for (let i = 0; i < length; i++) {
- let matches: RegExpExecArray | null;
- if ((matches = argPatterns[i].exec(args[i])) === null) {
- matched = false;
- break;
- }
- parsed.push(matches[0]);
- }
- }
- if (!length || matched) {
- await action(parsed);
- this.valid(`${command} ${parsed.join(" ")}`);
- return;
- }
- }
- this.invalid(command, true);
- } else {
- this.invalid(command, false);
- }
- }
-
-} \ No newline at end of file
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index cbe070293..9f67c1dda 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -86,7 +86,7 @@ function buildWithMiddleware(server: express.Express) {
resave: true,
cookie: { maxAge: week },
saveUninitialized: true,
- store: new MongoStore({ url: Database.url })
+ store: process.env.DB === "MEM" ? new session.MemoryStore() : new MongoStore({ url: Database.url })
}),
flash(),
expressFlash(),
@@ -152,4 +152,4 @@ function registerCorsProxy(server: express.Express) {
});
}).pipe(res);
});
-} \ No newline at end of file
+}