From 202333c8cd76270512dc339259bd830259a67b70 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Thu, 25 Feb 2021 15:37:39 -0700 Subject: [PATCH 01/15] implement XhrListener to forward POSTs to WS --- src/logger.ts | 31 +++++++++--- src/server.ts | 7 ++- src/xhrListener.ts | 120 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 src/xhrListener.ts diff --git a/src/logger.ts b/src/logger.ts index 3de53fc..e4b672f 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -7,27 +7,42 @@ const logPath = './logs/'; const tsFormat = () => (new Date().toISOString()); const logFormat = format.combine(format.timestamp(), format.json()); -function loggerTransports(logName: string, logLevel: string) { - return [ - new transports.File({ +function loggerTransports(logLevel: string, logName = "") { + let transport:any; + if ("" === logName) { + transport = new transports.Console({ + format: format.simple(), + level: logLevel, + timestamp: tsFormat + }); + } else { + transport = new transports.File({ filename: path.join(logPath, `${logName}.log`), timestamp: tsFormat, level: logLevel - }) - ]; + }); + } + + return [ transport ]; } +const debugLog = createLogger({ + format: format.simple(), + transports: loggerTransports('debug') +}); + const errorLog = createLogger({ format: logFormat, - transports: loggerTransports('errors', 'debug') + transports: loggerTransports('debug', 'errors') }); const accessLog = createLogger({ format: logFormat, - transports: loggerTransports('access', 'info') + transports: loggerTransports('info', 'access') }); module.exports = { errorLog, - accessLog + accessLog, + debugLog }; diff --git a/src/server.ts b/src/server.ts index 43923b1..0622a43 100755 --- a/src/server.ts +++ b/src/server.ts @@ -9,6 +9,7 @@ const logger = require('./logger'); import ClientManager from './clientManager'; import ChannelManager from './channelManager'; +import XhrListener from './xhrListener'; import PublicClient from './clients/types/publicClient'; import PrivateClient from './clients/types/privateClient'; import CustomClient from './clients/types/customClient'; @@ -16,6 +17,7 @@ import CustomClient from './clients/types/customClient'; const wss = new WebSocket.Server({ maxPayload: 250000, port: app.port }); const clientManager = new ClientManager(); const channelManager = new ChannelManager(); +const xhrListener = new XhrListener(channelManager); function connectionManager() { wss.on('connection', (ws: WebSocket, request: any, args: string) => { @@ -63,7 +65,7 @@ function connectionManager() { channelManager.purgeEmptyChannels(); - ws.send(`Hi there, welcome to braid, Measures Web Socket server. Connecting all our services!\nYou are currently in channel: ${data.channel}`); + ws.send(`Hi there! welcome to braid, Measures Web Socket server. Connecting all our services!\nYou are currently in channel: ${data.channel}`); } }); } @@ -88,5 +90,6 @@ startServer(); module.exports = { clientManager, channelManager, - connectionManager + connectionManager, + xhrListener }; diff --git a/src/xhrListener.ts b/src/xhrListener.ts new file mode 100644 index 0000000..0210060 --- /dev/null +++ b/src/xhrListener.ts @@ -0,0 +1,120 @@ +import ChannelManager from './channelManager' + +const logger = require('./logger'); +const url = require('url'); +const http = require('http'); +const crypto = require('crypto'); +const AUTH_TOKEN = process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'sha1=baddbeef' +const HTTP_PORT = process.env.HTTP_PORT || 80 + +import { Server, IncomingMessage, ServerResponse } from 'http' + +interface ArchimedesMessage { + examId: string; +} + +class XhrListener { + #cm: ChannelManager; + + constructor(cm: ChannelManager) { + // 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"); + } + + public relayEvent(event: string, channel: string):boolean { + logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${channel})`); + for (let c of this.#cm.channels) { + if (channel === c.id) { + let dmCount:number = 0; + for (let client of c.clients) { + let nonce:string = crypto.randomBytes(4).toString("hex"); + // TODO: verify the nonce against the received reply, which we currently ignore + client.directMessage(JSON.stringify({message_type:"broadcast", message:{event_type:event, seq_id:nonce}})); + dmCount++; + } + // dmCount of 1 would be normal. more than 1 is odd, but not necessarily bad. 0 means Exam UI has gone away somehow. + return dmCount > 0; + } + } + // 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"}' + 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.examId)) { + 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.examId)) { + 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; From f2ad95de0320cd2999f2544e41e2ef7582162b69 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Fri, 21 Feb 2020 15:33:06 -0500 Subject: [PATCH 02/15] jenkins change --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 077cc49..e25d96c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ podTemplate(label: label, inheritFrom: 'base', , containers: [ stage('Install Dependencies') { container('base') { sh "npm install" - sh "npm install -g typescript" + sh "npm install typescript" } } stage('Build the Project') { From 9a38d5f0a8ce16cbcc1359a4f49d0400fa765ee1 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Mon, 1 Mar 2021 12:05:41 -0700 Subject: [PATCH 03/15] shot in the dark to fix `tsc` failure during CI --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e25d96c..a66ceac 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,7 +17,7 @@ podTemplate(label: label, inheritFrom: 'base', , containers: [ } stage('Build the Project') { container('base') { - sh "tsc" + sh "./node_modules/tsc/bin/tsc" } } stage('Run unit tests') { @@ -44,4 +44,4 @@ podTemplate(label: label, inheritFrom: 'base', , containers: [ } } } -} \ No newline at end of file +} From eccba2876e1fb4fb26603034f346cc4ee0834577 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Mon, 1 Mar 2021 12:12:12 -0700 Subject: [PATCH 04/15] shot in the dark pt. 2 --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index a66ceac..e1bf986 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,7 +17,7 @@ podTemplate(label: label, inheritFrom: 'base', , containers: [ } stage('Build the Project') { container('base') { - sh "./node_modules/tsc/bin/tsc" + sh "npx tsc" } } stage('Run unit tests') { From 76e321c3f76c6add9eb34450b0ffa28c2fe7616a Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Tue, 2 Mar 2021 13:51:53 -0700 Subject: [PATCH 05/15] =?UTF-8?q?clean=20up=20indentation=20(blame/praise?= =?UTF-8?q?=20VS=20Code=20for=20this=20=F0=9F=98=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/xhrListener.ts | 78 +++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/xhrListener.ts b/src/xhrListener.ts index 0210060..a327875 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -26,26 +26,26 @@ class XhrListener { logger.debugLog.info("XhrListener running"); } - public relayEvent(event: string, channel: string):boolean { + public relayEvent(event: string, channel: string): boolean { logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${channel})`); for (let c of this.#cm.channels) { if (channel === c.id) { - let dmCount:number = 0; - for (let client of c.clients) { - let nonce:string = crypto.randomBytes(4).toString("hex"); - // TODO: verify the nonce against the received reply, which we currently ignore - client.directMessage(JSON.stringify({message_type:"broadcast", message:{event_type:event, seq_id:nonce}})); - dmCount++; - } - // dmCount of 1 would be normal. more than 1 is odd, but not necessarily bad. 0 means Exam UI has gone away somehow. - return dmCount > 0; + let dmCount: number = 0; + for (let client of c.clients) { + let nonce: string = crypto.randomBytes(4).toString("hex"); + // TODO: verify the nonce against the received reply, which we currently ignore + client.directMessage(JSON.stringify({ message_type: "broadcast", message: { event_type: event, seq_id: nonce } })); + dmCount++; + } + // dmCount of 1 would be normal. more than 1 is odd, but not necessarily bad. 0 means Exam UI has gone away somehow. + return dmCount > 0; } } // did not match a channel; examId isn't valid for some reason return false; } - public onRequestEnded(body: Uint8Array[], request: IncomingMessage, response: ServerResponse):void { + public onRequestEnded(body: Uint8Array[], request: IncomingMessage, response: ServerResponse): void { logger.debugLog.info("XhrListener:onRequestEnded()"); if (request.method === 'OPTIONS') { logger.debugLog.info("handling CORS preflight"); @@ -59,56 +59,56 @@ class XhrListener { 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; + let authorized: boolean = request.headers['x-proctoru-signature'] === AUTH_TOKEN; // body of POST is JSON: '{"examId":"proctor_u_id"}' switch (true) { - // match /event/pause* + // match /event/pause* case /^\/event\/pause([/]+.*)*$/.test(endpoint): - if (authorized) { - let amsg:ArchimedesMessage = JSON.parse(Buffer.concat(body).toString()); - if (this.relayEvent("pauseExam", amsg.examId)) { - returnVal = [200, "pause event was successfully relayed"]; + if (authorized) { + let amsg: ArchimedesMessage = JSON.parse(Buffer.concat(body).toString()); + if (this.relayEvent("pauseExam", amsg.examId)) { + 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"]; - } + logger.errorLog.info(`XhrListener: could not relay pause event "${amsg}" to Exam UI; body: "${body}"`); + returnVal = [400, "error while relaying pause event"]; + } } - break; + break; - // match /event/resume* + // match /event/resume* case /^\/event\/resume([/]+.*)*$/.test(endpoint): - if (authorized) { - let amsg:ArchimedesMessage = JSON.parse(Buffer.concat(body).toString()); - if (this.relayEvent("resumeExam", amsg.examId)) { - returnVal = [200, "resume event was successfully relayed"]; + if (authorized) { + let amsg: ArchimedesMessage = JSON.parse(Buffer.concat(body).toString()); + if (this.relayEvent("resumeExam", amsg.examId)) { + 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"]; - } + logger.errorLog.info(`XhrListener: could not relay resume event "${amsg}" to Exam UI; body: "${body}"`); + returnVal = [400, "error while relaying resume event"]; + } } - break; + break; - default: - logger.errorLog.info(`XhrListener: bad event or other request; body: "${body}"`); - returnVal = [400, "Bad request"]; - 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(); + response.writeHead(authorized ? 400 : 401); + response.end(); } else { - response.writeHead(returnVal[0], {'Content-Type': 'application/json'}); + response.writeHead(returnVal[0], { 'Content-Type': 'application/json' }); response.end(JSON.stringify(returnVal[1])); } } } - private onRequest(request: IncomingMessage, response: ServerResponse):void { + private onRequest(request: IncomingMessage, response: ServerResponse): void { logger.debugLog.info("XhrListener:onRequest()"); - let body:Uint8Array[] = []; + let body: Uint8Array[] = []; request.on('data', post_block => body.push(post_block)); request.on('end', () => { From 3e78524a6d8d1dfd252423c899a0fd9f6cc52efd Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Tue, 2 Mar 2021 14:28:23 -0700 Subject: [PATCH 06/15] =?UTF-8?q?version=201.2.3=20=E2=86=92=201.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docker_tag.txt | 2 +- src/config/app.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 77c8bc3..3e615f5 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# BRAID v1.2.3 +# BRAID v1.3.0 > Websocket server for the Measure platform [![Build Status](https://semaphoreci.com/api/v1/projects/7767f0f3-4da6-4c84-9167-4db5402a3262/2573412/badge.svg)](https://semaphoreci.com/yardstick/braid) diff --git a/docker_tag.txt b/docker_tag.txt index 0495c4a..f0bb29e 100644 --- a/docker_tag.txt +++ b/docker_tag.txt @@ -1 +1 @@ -1.2.3 +1.3.0 diff --git a/src/config/app.ts b/src/config/app.ts index 5887065..8cac077 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -1,5 +1,5 @@ module.exports = { - version : '1.2.3', + version : '1.3.0', whitelist : (process.env.WHITELIST || 'http://admin.localhost').split(','), secret : process.env.SECRET || 'test', devToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImNsaWVudCI6InRlc3QiLCJjbGllbnRfdHlwZSI6InNpdGUiLCJ1c2VyX3R5cGUiOiJ1c2VyIiwidXNlcl9pZCI6MjAwLCJjaGFubmVsIjoidGVzdF9jaGFubmVsIn0sImF1ZCI6ImludGVybmFsIiwiaXNzIjoiWWFyZHN0aWNrIFNvZnR3YXJlIiwic3ViIjoiQnJhaWQgSldUIn0.5KNCov_EW1cycT4Ay0oSvk4Z4PHFedd3bWOyqkHHTBQ', From 082b81598927167852afc493cf79dfcad72effc5 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Wed, 3 Mar 2021 08:51:13 -0700 Subject: [PATCH 07/15] commenting-out this.ws.ping test as it appears broken --- src/clients/clientBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clients/clientBase.ts b/src/clients/clientBase.ts index 4139ac7..1ed82bb 100644 --- a/src/clients/clientBase.ts +++ b/src/clients/clientBase.ts @@ -51,7 +51,7 @@ class ClientBase { logger.accessLog.info(`client (${this.id}) ponged.`); this.ws.pong(); } - this.heartbeat = setInterval(() => { this.ws.ping('ping') }, 30000); + //this.heartbeat = setInterval(() => { this.ws.ping('ping') }, 30000); logger.accessLog.info('Client Created', {data}); } From 60b5f21f762b0cc3479958cff1170c2d924f8227 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Wed, 3 Mar 2021 11:12:51 -0700 Subject: [PATCH 08/15] use config/apps.ts for configuration --- src/config/app.ts | 4 +++- src/xhrListener.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/config/app.ts b/src/config/app.ts index 8cac077..55fa5b3 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -4,6 +4,7 @@ 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', @@ -15,5 +16,6 @@ module.exports = { audience: 'internal', algorithm: ['HS256'] }, - messageTypes : ['broadcast', 'direct', 'changeChannel'] + messageTypes : ['broadcast', 'direct', 'changeChannel'], + archimedesToken: process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'sha1=baddbeef' }; diff --git a/src/xhrListener.ts b/src/xhrListener.ts index a327875..9730009 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -4,8 +4,10 @@ const logger = require('./logger'); const url = require('url'); const http = require('http'); const crypto = require('crypto'); -const AUTH_TOKEN = process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'sha1=baddbeef' -const HTTP_PORT = process.env.HTTP_PORT || 80 +const app = require('./config/app'); + +const AUTH_TOKEN = app.archimedesToken; +const HTTP_PORT = app.httpPort; import { Server, IncomingMessage, ServerResponse } from 'http' From 66b8ef945fde81c0382391bfa313bc04463be9b6 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Wed, 3 Mar 2021 13:59:14 -0700 Subject: [PATCH 09/15] pass along reservation_no from Archimedes --- src/xhrListener.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/xhrListener.ts b/src/xhrListener.ts index 9730009..1490810 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -12,7 +12,8 @@ const HTTP_PORT = app.httpPort; import { Server, IncomingMessage, ServerResponse } from 'http' interface ArchimedesMessage { - examId: string; + examId: string, + reservation_no: string } class XhrListener { @@ -28,15 +29,18 @@ class XhrListener { logger.debugLog.info("XhrListener running"); } - public relayEvent(event: string, channel: string): boolean { - logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${channel})`); + public relayEvent(event: string, archMsg: ArchimedesMessage): boolean { + logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${archMsg.examId})`); for (let c of this.#cm.channels) { - if (channel === c.id) { + if (archMsg.examId === c.id) { let dmCount: number = 0; for (let client of c.clients) { let nonce: string = crypto.randomBytes(4).toString("hex"); // TODO: verify the nonce against the received reply, which we currently ignore - client.directMessage(JSON.stringify({ message_type: "broadcast", message: { event_type: event, seq_id: nonce } })); + client.directMessage(JSON.stringify({ + message_type: "broadcast", + message: { event_type: event, seq_id: nonce, reservation_no: archMsg.reservation_no } + })); dmCount++; } // dmCount of 1 would be normal. more than 1 is odd, but not necessarily bad. 0 means Exam UI has gone away somehow. @@ -63,13 +67,13 @@ class XhrListener { let returnVal: [number, string] = [-1, ""]; let authorized: boolean = request.headers['x-proctoru-signature'] === AUTH_TOKEN; - // body of POST is JSON: '{"examId":"proctor_u_id"}' + // 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.examId)) { + 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}"`); @@ -83,7 +87,7 @@ class XhrListener { case /^\/event\/resume([/]+.*)*$/.test(endpoint): if (authorized) { let amsg: ArchimedesMessage = JSON.parse(Buffer.concat(body).toString()); - if (this.relayEvent("resumeExam", amsg.examId)) { + 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}"`); From 8ee1c6551f37dc725fcfe3c4a58c96d0c9b5841c Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Wed, 3 Mar 2021 14:14:01 -0700 Subject: [PATCH 10/15] ensure that channel is being looked up with reservation_no --- src/xhrListener.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xhrListener.ts b/src/xhrListener.ts index 1490810..03e35ab 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -30,16 +30,16 @@ class XhrListener { } public relayEvent(event: string, archMsg: ArchimedesMessage): boolean { - logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${archMsg.examId})`); + logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${archMsg.reservation_no})`); for (let c of this.#cm.channels) { - if (archMsg.examId === c.id) { + if (archMsg.reservation_no === c.id) { let dmCount: number = 0; for (let client of c.clients) { let nonce: string = crypto.randomBytes(4).toString("hex"); // TODO: verify the nonce against the received reply, which we currently ignore client.directMessage(JSON.stringify({ message_type: "broadcast", - message: { event_type: event, seq_id: nonce, reservation_no: archMsg.reservation_no } + message: { event_type: event, seq_id: nonce, examId: archMsg.examId } })); dmCount++; } From 72d7ffd5c0cd741c8fafa302c1f87a86e0d61a0d Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Thu, 11 Mar 2021 11:28:26 -0700 Subject: [PATCH 11/15] rewrite XhrListener to use Express --- src/config/app.ts | 1 - src/server.ts | 11 +++- src/xhrListener.ts | 125 +++++++++++++++++---------------------------- 3 files changed, 57 insertions(+), 80 deletions(-) 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; From 0a89138a793537b67c47c8df8e64c97dfc851acc Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Fri, 12 Mar 2021 10:26:43 -0700 Subject: [PATCH 12/15] compare channel id on value basis --- src/xhrListener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xhrListener.ts b/src/xhrListener.ts index 8b72dc7..39617e7 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -74,7 +74,7 @@ class XhrListener { public relayEvent(event: string, archMsg: ArchimedesMessage): boolean { logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${archMsg.reservation_no})`); for (let c of this.#cm.channels) { - if (archMsg.reservation_no === c.id) { + if (archMsg.reservation_no == c.id) { let dmCount: number = 0; for (let client of c.clients) { let nonce: string = crypto.randomBytes(4).toString("hex"); From 97a746c5729f4031ded987d392c67b5e4f017ee1 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Fri, 12 Mar 2021 11:08:19 -0700 Subject: [PATCH 13/15] don't double-encode JSON --- src/xhrListener.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xhrListener.ts b/src/xhrListener.ts index 39617e7..2322e98 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -79,10 +79,10 @@ class XhrListener { for (let client of c.clients) { let nonce: string = crypto.randomBytes(4).toString("hex"); // TODO: verify the nonce against the received reply, which we currently ignore - client.directMessage(JSON.stringify({ + client.directMessage({ message_type: "broadcast", message: { event_type: event, seq_id: nonce, examId: archMsg.examId } - })); + }); dmCount++; } // dmCount of 1 would be normal. more than 1 is odd, but not necessarily bad. 0 means Exam UI has gone away somehow. From 473fa4b66210dd32900d9af0d4e4b79a1ecf4c97 Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Fri, 12 Mar 2021 12:25:22 -0700 Subject: [PATCH 14/15] set default auth token to SHA-1 of empty string --- src/config/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/app.ts b/src/config/app.ts index 3bcdbf1..a6c01e5 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -16,5 +16,5 @@ module.exports = { algorithm: ['HS256'] }, messageTypes : ['broadcast', 'direct', 'changeChannel'], - archimedesToken: process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'sha1=baddbeef' + archimedesToken: process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'sha1=da39a3ee5e6b4b0d3255bfef95601890afd80709' }; From 7ba8716917f590c3c5db2950fde8e2d13b2d9b3f Mon Sep 17 00:00:00 2001 From: Maciek Nowacki Date: Fri, 12 Mar 2021 15:10:49 -0700 Subject: [PATCH 15/15] compare auth token against SHA-1 HMAC of POST body --- src/config/app.ts | 2 +- src/xhrListener.ts | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/config/app.ts b/src/config/app.ts index a6c01e5..7c8c30b 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -16,5 +16,5 @@ module.exports = { algorithm: ['HS256'] }, messageTypes : ['broadcast', 'direct', 'changeChannel'], - archimedesToken: process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'sha1=da39a3ee5e6b4b0d3255bfef95601890afd80709' + archimedesToken: process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'testkeyformeasure' }; diff --git a/src/xhrListener.ts b/src/xhrListener.ts index 2322e98..ecfa670 100644 --- a/src/xhrListener.ts +++ b/src/xhrListener.ts @@ -11,7 +11,7 @@ const AUTH_TOKEN = app.archimedesToken; const HTTP_PORT = app.port; import { Server, IncomingMessage, ServerResponse } from 'http' - +import * as bodyParser from "body-parser"; interface ArchimedesMessage { examId: string, reservation_no: string @@ -25,8 +25,12 @@ class XhrListener { this.#cm = cm; logger.debugLog.info("XhrListener running"); - app.use(jsonParser); + //app.use(jsonParser); + // turns out that we must compute HMAC based on the payload of the POST body, so: + app.use(bodyParser.json({ + verify: ((req: any, res: any, buf: any) => { req.rawBody = buf }) + })); // TODO: try to pin down origin to reflect client rather than use wildcard app.options('*', cors()); @@ -39,19 +43,22 @@ class XhrListener { 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 authorized(authOffered: String, rawBody: Buffer): boolean { + var hash = crypto.createHmac('sha1', AUTH_TOKEN).update(rawBody).digest("hex"); + + return authOffered.split("=").reverse()[0] === hash; } public eventFromArchimedes(eventType: string, req: any, res: any) { logger.debugLog.info(`eventFromArchimedes("${eventType}") called`); - let authorized: boolean = this.authorized(req.headers['x-proctoru-signature']); + let authorized: boolean = this.authorized(req.headers['x-proctoru-signature'], req.rawBody); if (!authorized) { res.status(401);