diff --git a/src/config/app.ts b/src/config/app.ts index 55fa5b3..3bcdbf1 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -4,7 +4,6 @@ module.exports = { secret : process.env.SECRET || 'test', devToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImNsaWVudCI6InRlc3QiLCJjbGllbnRfdHlwZSI6InNpdGUiLCJ1c2VyX3R5cGUiOiJ1c2VyIiwidXNlcl9pZCI6MjAwLCJjaGFubmVsIjoidGVzdF9jaGFubmVsIn0sImF1ZCI6ImludGVybmFsIiwiaXNzIjoiWWFyZHN0aWNrIFNvZnR3YXJlIiwic3ViIjoiQnJhaWQgSldUIn0.5KNCov_EW1cycT4Ay0oSvk4Z4PHFedd3bWOyqkHHTBQ', port: process.env.PORT || 80, - httpPort: process.env.HTTP_PORT || 8080, hostname: process.env.HOSTNAME || 'ysbraid.localhost', environment: process.env.ENVIRONMENT || 'development', log_level: process.env.LOG_LEVEL || 'debug', diff --git a/src/server.ts b/src/server.ts index 0622a43..d2a83e6 100755 --- a/src/server.ts +++ b/src/server.ts @@ -2,10 +2,12 @@ import * as WebSocket from 'ws'; import * as jwt from 'jsonwebtoken'; import * as url from 'url'; +import * as express from 'express'; // internal imports const app = require('./config/app'); const logger = require('./logger'); +const http = require('http'); import ClientManager from './clientManager'; import ChannelManager from './channelManager'; @@ -14,10 +16,15 @@ import PublicClient from './clients/types/publicClient'; import PrivateClient from './clients/types/privateClient'; import CustomClient from './clients/types/customClient'; -const wss = new WebSocket.Server({ maxPayload: 250000, port: app.port }); +const appExpress = express(); +const server = new http.createServer(); +// the port is no longer specified here as it induces WS to start running on its own +// instead, we do it later in XhrListener +const wss = new WebSocket.Server({ server: server, maxPayload: 250000 }); + const clientManager = new ClientManager(); const channelManager = new ChannelManager(); -const xhrListener = new XhrListener(channelManager); +const xhrListener = new XhrListener(channelManager, server, appExpress, express.json({type: 'application/json'})); function connectionManager() { wss.on('connection', (ws: WebSocket, request: any, args: string) => { diff --git a/src/xhrListener.ts b/src/xhrListener.ts index 03e35ab..8b72dc7 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -5,9 +5,10 @@ const url = require('url'); const http = require('http'); const crypto = require('crypto'); const app = require('./config/app'); +const cors = require('cors'); const AUTH_TOKEN = app.archimedesToken; -const HTTP_PORT = app.httpPort; +const HTTP_PORT = app.port; import { Server, IncomingMessage, ServerResponse } from 'http' @@ -19,14 +20,55 @@ interface ArchimedesMessage { class XhrListener { #cm: ChannelManager; - constructor(cm: ChannelManager) { + constructor(cm: ChannelManager, server: any, app: any, jsonParser: any) { + + this.#cm = cm; + logger.debugLog.info("XhrListener running"); + + app.use(jsonParser); + + // TODO: try to pin down origin to reflect client rather than use wildcard + app.options('*', cors()); + // I'm not entirely happy with bind(this) but it does let me break the // callback and event handlers out into class methods. It will be // problematic if the actual `this` needs to be accessed. - let xhrlisten = http.createServer(this.onRequest.bind(this)); - xhrlisten.listen(HTTP_PORT); - this.#cm = cm; - logger.debugLog.info("XhrListener running"); + + app.post('/event/resume', cors(), + (req: any, res: any, next: any) => this.eventFromArchimedes.bind(this)("resumeExam", req, res)); + app.post('/event/pause', cors(), + (req: any, res: any, next: any) => this.eventFromArchimedes.bind(this)("pauseExam", req, res)); + + server.on('request', app); + + // note that this kicks off HTTP listen for the entire app - not just HTTP but WS as well! + server.listen(HTTP_PORT, () => logger.debugLog.info(`braid is now listening for HTTP and WS connections on port ${HTTP_PORT}`)); + } + + public authorized(authOffered: String): boolean { + return AUTH_TOKEN === authOffered; + } + + public eventFromArchimedes(eventType: string, req: any, res: any) { + logger.debugLog.info(`eventFromArchimedes("${eventType}") called`); + let authorized: boolean = this.authorized(req.headers['x-proctoru-signature']); + + if (!authorized) { + res.status(401); + res.send("Unauthorized"); + return; + } + + res.setHeader('Content-Type', 'application/json'); + let amsg: ArchimedesMessage = { 'examId': req.body.examId, 'reservation_no': req.body.reservation_no }; + if (this.relayEvent(eventType, amsg)) { + logger.debugLog.info('200 success relaying ' + eventType); + res.send(JSON.stringify(`${eventType} event was successfully relayed`)); + } else { + logger.errorLog.info(`XhrListener: could not relay ${eventType} event "${amsg}" to Exam UI; body: "${req.body}"`); + res.status(400); + res.send(JSON.stringify(`error while relaying ${eventType} event`)); + } } public relayEvent(event: string, archMsg: ArchimedesMessage): boolean { @@ -50,77 +92,6 @@ class XhrListener { // did not match a channel; examId isn't valid for some reason return false; } - - public onRequestEnded(body: Uint8Array[], request: IncomingMessage, response: ServerResponse): void { - logger.debugLog.info("XhrListener:onRequestEnded()"); - if (request.method === 'OPTIONS') { - logger.debugLog.info("handling CORS preflight"); - // CORS: implement permissive policy for XHR clients - response.setHeader('Access-Control-Allow-Origin', request.headers['origin'] || "*"); - response.setHeader('Access-Control-Allow-Methods', request.headers['access-control-request-methods'] || "OPTIONS, GET, POST"); - response.setHeader('Access-Control-Allow-Headers', request.headers['access-control-request-headers'] || "Content-Type"); - response.setHeader('Access-Control-Allow-Credentials', "true"); - response.end(); - } else { - logger.debugLog.info("parsing Archimedes event: " + body); - let endpoint = url.parse(request.url).pathname - let returnVal: [number, string] = [-1, ""]; - let authorized: boolean = request.headers['x-proctoru-signature'] === AUTH_TOKEN; - - // body of POST is JSON: '{"examId":"proctor_u_id", "reservation_no":"primary_key_from_archimedes"}' - switch (true) { - // match /event/pause* - case /^\/event\/pause([/]+.*)*$/.test(endpoint): - if (authorized) { - let amsg: ArchimedesMessage = JSON.parse(Buffer.concat(body).toString()); - if (this.relayEvent("pauseExam", amsg)) { - returnVal = [200, "pause event was successfully relayed"]; - } else { - logger.errorLog.info(`XhrListener: could not relay pause event "${amsg}" to Exam UI; body: "${body}"`); - returnVal = [400, "error while relaying pause event"]; - } - } - - break; - - // match /event/resume* - case /^\/event\/resume([/]+.*)*$/.test(endpoint): - if (authorized) { - let amsg: ArchimedesMessage = JSON.parse(Buffer.concat(body).toString()); - if (this.relayEvent("resumeExam", amsg)) { - returnVal = [200, "resume event was successfully relayed"]; - } else { - logger.errorLog.info(`XhrListener: could not relay resume event "${amsg}" to Exam UI; body: "${body}"`); - returnVal = [400, "error while relaying resume event"]; - } - } - break; - - default: - logger.errorLog.info(`XhrListener: bad event or other request; body: "${body}"`); - returnVal = [400, "Bad request"]; - break; - }; - - if (-1 == returnVal[0]) { - response.writeHead(authorized ? 400 : 401); - response.end(); - } else { - response.writeHead(returnVal[0], { 'Content-Type': 'application/json' }); - response.end(JSON.stringify(returnVal[1])); - } - } - } - - private onRequest(request: IncomingMessage, response: ServerResponse): void { - logger.debugLog.info("XhrListener:onRequest()"); - let body: Uint8Array[] = []; - request.on('data', post_block => body.push(post_block)); - - request.on('end', () => { - this.onRequestEnded(body, request, response); - }); - } } export default XhrListener;