braid/src/xhrListener.ts
2021-03-03 13:59:14 -07:00

127 lines
4.9 KiB
TypeScript

import ChannelManager from './channelManager'
const logger = require('./logger');
const url = require('url');
const http = require('http');
const crypto = require('crypto');
const app = require('./config/app');
const AUTH_TOKEN = app.archimedesToken;
const HTTP_PORT = app.httpPort;
import { Server, IncomingMessage, ServerResponse } from 'http'
interface ArchimedesMessage {
examId: string,
reservation_no: 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, archMsg: ArchimedesMessage): boolean {
logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${archMsg.examId})`);
for (let c of this.#cm.channels) {
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, 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.
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", "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;