aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/ApiManagers/DeleteManager.ts12
-rw-r--r--src/server/ApiManagers/UploadManager.ts64
-rw-r--r--src/server/ApiManagers/UtilManager.ts3
-rw-r--r--src/server/DashUploadUtils.ts2
-rw-r--r--src/server/RouteManager.ts15
-rw-r--r--src/server/apis/google/CredentialsLoader.ts40
-rw-r--r--src/server/index.ts45
-rw-r--r--src/server/server_Initialization.ts37
-rw-r--r--src/server/websocket.ts35
9 files changed, 194 insertions, 59 deletions
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index 7fbb37658..46c0d8a8a 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -14,24 +14,20 @@ export default class DeleteManager extends ApiManager {
register({
method: Method.GET,
+ requireAdminInRelease: true,
subscription: new RouteSubscriber("delete").add("target?"),
- secureHandler: async ({ req, res, isRelease }) => {
- if (isRelease) {
- return _permission_denied(res, "Cannot perform a delete operation outside of the development environment!");
- }
-
+ secureHandler: async ({ req, res }) => {
const { target } = req.params;
- const { doDelete } = WebSocket;
if (!target) {
- await doDelete();
+ await WebSocket.doDelete();
} else {
let all = false;
switch (target) {
case "all":
all = true;
case "database":
- await doDelete(false);
+ await WebSocket.doDelete(false);
if (!all) break;
case "files":
rimraf.sync(filesDirectory);
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index b185d3b55..06c2e516d 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -4,14 +4,18 @@ import * as formidable from 'formidable';
import v4 = require('uuid/v4');
const AdmZip = require('adm-zip');
import { extname, basename, dirname } from 'path';
-import { createReadStream, createWriteStream, unlink } from "fs";
+import { createReadStream, createWriteStream, unlink, writeFile } from "fs";
import { publicDirectory, filesDirectory } from "..";
import { Database } from "../database";
import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
import * as sharp from 'sharp';
import { AcceptibleMedia, Upload } from "../SharedMediaTypes";
import { normalize } from "path";
+import RouteSubscriber from "../RouteSubscriber";
const imageDataUri = require('image-data-uri');
+import { isWebUri } from "valid-url";
+import { launch } from "puppeteer";
+import { Opt } from "../../fields/Doc";
export enum Directory {
parsed_files = "parsed_files",
@@ -61,10 +65,38 @@ export default class UploadManager extends ApiManager {
});
register({
- method: Method.GET,
- subscription: "/hello",
- secureHandler: ({ req, res }) => {
- res.send("<h1>world!</h1>");
+ method: Method.POST,
+ subscription: new RouteSubscriber("youtubeScreenshot"),
+ secureHandler: async ({ req, res }) => {
+ const { url, t } = req.query;
+ const parseUrl = (url: string) => {
+ url = decodeURIComponent(url)
+ if (!/^(?:f|ht)tps?\:\/\//.test(url)) {
+ url = 'http://' + url
+ }
+ return url;
+ }
+ let targetUrl: string;
+ if (!url || !t || !isWebUri(targetUrl = parseUrl(url as string))) {
+ return res.send();
+ }
+ const buffer = await captureYoutubeScreenshot(targetUrl, t as string);
+ if (!buffer) {
+ return res.send();
+ }
+ const resolvedName = `${targetUrl}@${t}.png`;
+ const resolvedPath = serverPathToFile(Directory.images, resolvedName);
+ writeFile(resolvedPath, buffer, async error => {
+ if (error) {
+ return res.send();
+ }
+ await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
+ res.send({
+ accessPaths: {
+ agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
+ }
+ } as Upload.FileInformation);
+ });
}
});
@@ -244,4 +276,26 @@ export default class UploadManager extends ApiManager {
}
+}
+
+/**
+ * On success, returns a buffer containing the bytes of a screenshot
+ * of the video specified by @param url at timecode @param t.
+ *
+ * On failure, returns undefined.
+ */
+async function captureYoutubeScreenshot(targetUrl: string, t: string): Promise<Opt<Buffer>> {
+ const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
+ const page = await browser.newPage();
+ await page.setViewport({ width: 1920, height: 1080 });
+
+ await page.goto(targetUrl + '&t=' + t, { waitUntil: 'networkidle0' });
+
+ // hide youtube player controls.
+ await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as any).style.display = 'none');
+
+ const buffer = await (await page.$('.html5-video-player'))?.screenshot({ encoding: "binary" });
+ await browser.close();
+
+ return buffer;
} \ No newline at end of file
diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts
index aec523cd0..e2cd88726 100644
--- a/src/server/ApiManagers/UtilManager.ts
+++ b/src/server/ApiManagers/UtilManager.ts
@@ -1,8 +1,6 @@
import ApiManager, { Registration } from "./ApiManager";
import { Method } from "../RouteManager";
import { exec } from 'child_process';
-import RouteSubscriber from "../RouteSubscriber";
-import { red } from "colors";
// import { IBM_Recommender } from "../../client/apis/IBM_Recommender";
// import { Recommender } from "../Recommender";
@@ -34,7 +32,6 @@ export default class UtilManager extends ApiManager {
// }
// });
-
register({
method: Method.GET,
subscription: "/pull",
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index b74904ada..c887aa4e6 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -257,7 +257,7 @@ export namespace DashUploadUtils {
});
}
- function getAccessPaths(directory: Directory, fileName: string) {
+ export function getAccessPaths(directory: Directory, fileName: string) {
return {
client: clientPathToFile(directory, fileName),
server: serverPathToFile(directory, fileName)
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index b23215996..1a2340afc 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -2,6 +2,7 @@ import RouteSubscriber from "./RouteSubscriber";
import { DashUserModel } from "./authentication/DashUserModel";
import { Request, Response, Express } from 'express';
import { cyan, red, green } from 'colors';
+import { AdminPriviliges } from ".";
export enum Method {
GET,
@@ -25,6 +26,7 @@ export interface RouteInitializer {
secureHandler: SecureHandler;
publicHandler?: PublicHandler;
errorHandler?: ErrorHandler;
+ requireAdminInRelease?: true;
}
const registered = new Map<string, Set<Method>>();
@@ -84,7 +86,7 @@ export default class RouteManager {
* @param initializer
*/
addSupervisedRoute = (initializer: RouteInitializer): void => {
- const { method, subscription, secureHandler, publicHandler, errorHandler } = initializer;
+ const { method, subscription, secureHandler, publicHandler, errorHandler, requireAdminInRelease: requireAdmin } = initializer;
typeof (initializer.subscription) === "string" && RouteManager.routes.push(initializer.subscription);
initializer.subscription instanceof RouteSubscriber && RouteManager.routes.push(initializer.subscription.root);
@@ -94,7 +96,7 @@ export default class RouteManager {
});
const isRelease = this._isRelease;
const supervised = async (req: Request, res: Response) => {
- let { user } = req;
+ let user = req.user as Partial<DashUserModel> | undefined;
const { originalUrl: target } = req;
if (process.env.DB === "MEM" && !user) {
user = { id: "guest", email: "", userDocumentId: "guestDocId" };
@@ -113,6 +115,13 @@ export default class RouteManager {
}
};
if (user) {
+ if (requireAdmin && isRelease && process.env.PASSWORD) {
+ if (AdminPriviliges.get(user.id)) {
+ AdminPriviliges.delete(user.id);
+ } else {
+ return res.redirect(`/admin/${req.originalUrl.substring(1).replace("/", ":")}`);
+ }
+ }
await tryExecute(secureHandler, { ...core, user });
} else {
req.session!.target = target;
@@ -205,5 +214,5 @@ export function _permission_denied(res: Response, message?: string) {
if (message) {
res.statusMessage = message;
}
- res.status(STATUS.PERMISSION_DENIED).send("Permission Denied!");
+ res.status(STATUS.PERMISSION_DENIED).send(`Permission Denied! ${message}`);
}
diff --git a/src/server/apis/google/CredentialsLoader.ts b/src/server/apis/google/CredentialsLoader.ts
index e3f4d167b..ef1f9a91e 100644
--- a/src/server/apis/google/CredentialsLoader.ts
+++ b/src/server/apis/google/CredentialsLoader.ts
@@ -1,4 +1,7 @@
-import { readFile } from "fs";
+import { readFile, readFileSync } from "fs";
+import { pathFromRoot } from "../../ActionUtilities";
+import { SecureContextOptions } from "tls";
+import { blue, red } from "colors";
export namespace GoogleCredentialsLoader {
@@ -27,3 +30,38 @@ export namespace GoogleCredentialsLoader {
}
}
+
+export namespace SSL {
+
+ export let Credentials: SecureContextOptions = {};
+ export let Loaded = false;
+
+ const suffixes = {
+ privateKey: ".key",
+ certificate: ".crt",
+ caBundle: "-ca.crt"
+ };
+
+ export async function loadCredentials() {
+ const { serverName } = process.env;
+ const cert = (suffix: string) => readFileSync(pathFromRoot(`./${serverName}${suffix}`)).toString();
+ try {
+ Credentials.key = cert(suffixes.privateKey);
+ Credentials.cert = cert(suffixes.certificate);
+ Credentials.ca = cert(suffixes.caBundle);
+ Loaded = true;
+ } catch (e) {
+ Credentials = {};
+ Loaded = false;
+ }
+ }
+
+ export function exit() {
+ console.log(red("Running this server in release mode requires the following SSL credentials in the project root:"));
+ const serverName = process.env.serverName ? process.env.serverName : "{process.env.serverName}";
+ Object.values(suffixes).forEach(suffix => console.log(blue(`${serverName}${suffix}`)));
+ console.log(red("Please ensure these files exist and restart, or run this in development mode."));
+ process.exit(0);
+ }
+
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index 7ac032ed3..ff74cfb33 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -11,9 +11,8 @@ import * as qs from 'query-string';
import UtilManager from './ApiManagers/UtilManager';
import { SearchManager } from './ApiManagers/SearchManager';
import UserManager from './ApiManagers/UserManager';
-import { WebSocket } from './websocket';
import DownloadManager from './ApiManagers/DownloadManager';
-import { GoogleCredentialsLoader } from './apis/google/CredentialsLoader';
+import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader';
import DeleteManager from "./ApiManagers/DeleteManager";
import PDFManager from "./ApiManagers/PDFManager";
import UploadManager from "./ApiManagers/UploadManager";
@@ -26,6 +25,7 @@ import { DashSessionAgent } from "./DashSession/DashSessionAgent";
import SessionManager from "./ApiManagers/SessionManager";
import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent";
+export const AdminPriviliges: Map<string, boolean> = new Map();
export const onWindows = process.platform === "win32";
export let sessionAgent: AppliedSessionAgent;
export const publicDirectory = path.resolve(__dirname, "public");
@@ -41,6 +41,7 @@ async function preliminaryFunctions() {
await DashUploadUtils.buildFileDirectories();
await Logger.initialize();
await GoogleCredentialsLoader.loadCredentials();
+ SSL.loadCredentials();
GoogleApiServerUtils.processProjectCredentials();
if (process.env.DB !== "MEM") {
await log_execution({
@@ -101,6 +102,42 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
};
+ /**
+ * Serves a simple password input box for any
+ */
+ addSupervisedRoute({
+ method: Method.GET,
+ subscription: new RouteSubscriber("admin").add("previous_target"),
+ secureHandler: ({ res, isRelease }) => {
+ const { PASSWORD } = process.env;
+ if (!(isRelease && PASSWORD)) {
+ return res.redirect("/home");
+ }
+ res.render("admin.pug", { title: "Enter Administrator Password" });
+ }
+ });
+
+ addSupervisedRoute({
+ method: Method.POST,
+ subscription: new RouteSubscriber("admin").add("previous_target"),
+ secureHandler: async ({ req, res, isRelease, user: { id } }) => {
+ const { PASSWORD } = process.env;
+ if (!(isRelease && PASSWORD)) {
+ return res.redirect("/home");
+ }
+ const { password } = req.body;
+ const { previous_target } = req.params;
+ let redirect: string;
+ if (password === PASSWORD) {
+ AdminPriviliges.set(id, true);
+ redirect = `/${previous_target.replace(":", "/")}`;
+ } else {
+ redirect = `/admin/${previous_target}`;
+ }
+ res.redirect(redirect);
+ }
+ });
+
addSupervisedRoute({
method: Method.GET,
subscription: ["/home", new RouteSubscriber("doc").add("docId")],
@@ -121,10 +158,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
});
logRegistrationOutcome();
-
- // initialize the web socket (bidirectional communication: if a user changes
- // a field on one client, that change must be broadcast to all other clients)
- WebSocket.initialize(isRelease);
}
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index f572ec906..14b8776d8 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -10,6 +10,7 @@ import { Database } from './database';
import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager';
const MongoStore = require('connect-mongo')(session);
import RouteManager from './RouteManager';
+import { WebSocket } from './websocket';
import * as webpack from 'webpack';
const config = require('../../webpack.config');
const compiler = webpack(config);
@@ -20,10 +21,11 @@ import * as request from 'request';
import RouteSubscriber from './RouteSubscriber';
import { publicDirectory } from '.';
import { logPort, pathFromRoot, } from './ActionUtilities';
-import { blue, yellow } from 'colors';
+import { blue, yellow, red } from 'colors';
import * as cors from "cors";
-import { createServer, Server as SecureServer } from "https";
-import { Server } from "http";
+import { createServer, Server as HttpsServer } from "https";
+import { Server as HttpServer } from "http";
+import { SSL } from './apis/google/CredentialsLoader';
/* RouteSetter is a wrapper around the server that prevents the server
from being exposed. */
@@ -47,28 +49,23 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
const isRelease = determineEnvironment();
+ isRelease && !SSL.Loaded && SSL.exit();
+
routeSetter(new RouteManager(app, isRelease));
registerRelativePath(app);
+ let server: HttpServer | HttpsServer;
const { serverPort } = process.env;
const resolved = isRelease && serverPort ? Number(serverPort) : 1050;
-
- let server: Server | SecureServer;
- if (isRelease) {
- server = createServer({
- key: fs.readFileSync(pathFromRoot(`./${process.env.serverName}.key`)),
- cert: fs.readFileSync(pathFromRoot(`./${process.env.serverName}.crt`))
- }, app);
- (server as SecureServer).listen(resolved, () => {
- logPort("server", resolved);
- console.log();
- });
- } else {
- server = app.listen(resolved, () => {
- logPort("server", resolved);
- console.log();
- });
- }
+ await new Promise<void>(resolve => server = isRelease ?
+ createServer(SSL.Credentials, app).listen(resolved, resolve) :
+ app.listen(resolved, resolve)
+ );
+ logPort("server", resolved);
+
+ // initialize the web socket (bidirectional communication: if a user changes
+ // a field on one client, that change must be broadcast to all other clients)
+ WebSocket.initialize(isRelease, app);
disconnect = async () => new Promise<Error>(resolve => server.close(resolve));
return isRelease;
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 9ab3d2611..21e772e83 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -1,18 +1,21 @@
+import * as fs from 'fs';
+import { logPort } from './ActionUtilities';
import { Utils } from "../Utils";
import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent, RoomMessage } from "./Message";
import { Client } from "./Client";
import { Socket } from "socket.io";
import { Database } from "./database";
import { Search } from "./Search";
-import * as io from 'socket.io';
+import * as sio from 'socket.io';
import YoutubeApi from "./apis/youtube/youtubeApiSample";
-import { GoogleCredentialsLoader } from "./apis/google/CredentialsLoader";
-import { logPort } from "./ActionUtilities";
+import { GoogleCredentialsLoader, SSL } from "./apis/google/CredentialsLoader";
import { timeMap } from "./ApiManagers/UserManager";
import { green } from "colors";
import { networkInterfaces } from "os";
import executeImport from "../scraping/buxton/final/BuxtonImporter";
import { DocumentsCollection } from "./IDatabase";
+import { createServer, Server } from "https";
+import * as express from "express";
export namespace WebSocket {
@@ -20,12 +23,25 @@ export namespace WebSocket {
const clients: { [key: string]: Client } = {};
export const socketMap = new Map<SocketIO.Socket, string>();
export let disconnect: Function;
+ const defaultPort = 4321;
+
+ export async function initialize(isRelease: boolean, app: express.Express) {
+ let io: sio.Server;
+ let resolved: number;
+ if (isRelease) {
+ const { socketPort } = process.env;
+ resolved = socketPort ? Number(socketPort) : defaultPort;
+ let socketEndpoint: Server;
+ await new Promise<void>(resolve => socketEndpoint = createServer(SSL.Credentials, app).listen(resolved, resolve));
+ io = sio(socketEndpoint!, SSL.Credentials as any);
+ } else {
+ io = sio().listen(resolved = defaultPort);
+ }
+ logPort("websocket", resolved);
+ console.log();
- export function initialize(isRelease: boolean) {
- const endpoint = io();
- endpoint.on("connection", function (socket: Socket) {
+ io.on("connection", function (socket: Socket) {
_socket = socket;
-
socket.use((_packet, next) => {
const userEmail = socketMap.get(socket);
if (userEmail) {
@@ -121,11 +137,6 @@ export namespace WebSocket {
socket.disconnect(true);
};
});
-
- const { socketPort } = process.env;
- const resolved = isRelease && socketPort ? Number(socketPort) : 4321;
- endpoint.listen(resolved);
- logPort("websocket", resolved);
}
function processGesturePoints(socket: Socket, content: GestureContent) {