diff options
Diffstat (limited to 'src/server')
-rw-r--r-- | src/server/ApiManagers/DeleteManager.ts | 12 | ||||
-rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 74 | ||||
-rw-r--r-- | src/server/ApiManagers/UtilManager.ts | 3 | ||||
-rw-r--r-- | src/server/DashUploadUtils.ts | 2 | ||||
-rw-r--r-- | src/server/RouteManager.ts | 15 | ||||
-rw-r--r-- | src/server/apis/google/CredentialsLoader.ts | 40 | ||||
-rw-r--r-- | src/server/index.ts | 57 | ||||
-rw-r--r-- | src/server/server_Initialization.ts | 59 | ||||
-rw-r--r-- | src/server/websocket.ts | 34 |
9 files changed, 224 insertions, 72 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..756bde738 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 { id, timecode } = req.body; + const convert = (raw: string) => { + const number = Math.floor(Number(raw)); + const seconds = number % 60; + const minutes = (number - seconds) / 60; + return `${minutes}m${seconds}s`; + }; + const suffix = timecode ? `&t=${convert(timecode)}` : ``; + const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`; + const buffer = await captureYoutubeScreenshot(targetUrl); + if (!buffer) { + return res.send(); + } + const resolvedName = `youtube_capture_${id}_${suffix}.png`; + const resolvedPath = serverPathToFile(Directory.images, resolvedName); + return new Promise<void>(resolve => { + 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); + resolve(); + }); + }); } }); @@ -244,4 +276,36 @@ export default class UploadManager extends ApiManager { } +} +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +/** + * On success, returns a buffer containing the bytes of a screenshot + * of the video (optionally, at a timecode) specified by @param targetUrl. + * + * On failure, returns undefined. + */ +async function captureYoutubeScreenshot(targetUrl: 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, { waitUntil: 'domcontentloaded' as any }); + + const videoPlayer = await page.$('.html5-video-player'); + videoPlayer && await page.focus("video"); + await delay(7000); + const ad = await page.$('.ytp-ad-skip-button-text'); + await ad?.click(); + await videoPlayer?.click(); + await delay(1000); + // hide youtube player controls. + await page.evaluate(() => + (document.querySelector('.ytp-chrome-bottom') as any).style.display = 'none'); + + const buffer = await videoPlayer?.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 af8f95a5e..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); } @@ -149,9 +182,9 @@ export async function launchServer() { * log the output of the server process, so it's not ideal for development. * So, the 'else' clause is exactly what we've always run when executing npm start. */ -if (process.env.RELEASE) { - (sessionAgent = new DashSessionAgent()).launch(); -} else { - (Database.Instance as Database.Database).doConnect(); - launchServer(); -} +// if (process.env.RELEASE) { +// (sessionAgent = new DashSessionAgent()).launch(); +// } else { +(Database.Instance as Database.Database).doConnect(); +launchServer(); +// } diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 14b0dedc3..d370385b2 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); @@ -19,9 +20,12 @@ import * as fs from 'fs'; import * as request from 'request'; import RouteSubscriber from './RouteSubscriber'; import { publicDirectory } from '.'; -import { logPort, } from './ActionUtilities'; -import { blue, yellow } from 'colors'; +import { logPort, pathFromRoot, } from './ActionUtilities'; +import { blue, yellow, red } from 'colors'; import * as cors from "cors"; +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. */ @@ -45,33 +49,25 @@ export default async function InitializeServer(routeSetter: RouteSetter) { const isRelease = determineEnvironment(); + isRelease && !SSL.Loaded && SSL.exit(); + routeSetter(new RouteManager(app, isRelease)); registerRelativePath(app); - const serverPort = isRelease ? Number(process.env.serverPort) : 1050; - const server = app.listen(serverPort, () => { - logPort("server", serverPort); - console.log(); - }); + let server: HttpServer | HttpsServer; + const { serverPort } = process.env; + const resolved = isRelease && serverPort ? Number(serverPort) : 1050; + await new Promise<void>(resolve => server = isRelease ? + createServer(SSL.Credentials, app).listen(resolved, resolve) : + app.listen(resolved, resolve) + ); + logPort("server", resolved); - // var express = require('express') - // var fs = require('fs') - // var https = require('https') - // var app = express() - - // app.get('/', function (req, res) { - // res.send('hello world') - // }) - - // https.createServer({ - // key: fs.readFileSync('server.key'), - // cert: fs.readFileSync('server.cert') - // }, app) - // .listen(3000, function () { - // console.log('Example app listening on port 3000! Go to https://localhost:3000/') - // }) - disconnect = async () => new Promise<Error>(resolve => server.close(resolve)); + // 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; } @@ -139,7 +135,7 @@ function registerAuthenticationRoutes(server: express.Express) { function registerCorsProxy(server: express.Express) { const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; - server.use("/corsProxy", (req, res) => { + server.use("/corsProxy", async (req, res) => { const requrl = decodeURIComponent(req.url.substring(1)); const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : ""; @@ -148,8 +144,15 @@ function registerCorsProxy(server: express.Express) { // then we redirect again to the cors referer and just add the relative path. if (!requrl.startsWith("http") && req.originalUrl.startsWith("/corsProxy") && referer?.includes("corsProxy")) { res.redirect(referer + (referer.endsWith("/") ? "" : "/") + requrl); - } - else { + } else { + try { + await new Promise<void>((resolve, reject) => { + request(requrl).on("response", resolve).on("error", reject); + }); + } catch { + console.log(`Malformed CORS url: ${requrl}`); + return res.send(); + } req.pipe(request(requrl)).on("response", res => { const headers = Object.keys(res.headers); headers.forEach(headerName => { @@ -162,7 +165,7 @@ function registerCorsProxy(server: express.Express) { } } }); - }).pipe(res); + }).on("error", () => console.log(`Malformed CORS url: ${requrl}`)).pipe(res); } }); } diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 7278bdc32..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,10 +137,6 @@ export namespace WebSocket { socket.disconnect(true); }; }); - - const socketPort = isRelease ? Number(process.env.socketPort) : 4321; - endpoint.listen(socketPort); - logPort("websocket", socketPort); } function processGesturePoints(socket: Socket, content: GestureContent) { |