rewrite XhrListener to use Express
This commit is contained in:
parent
8ee1c6551f
commit
72d7ffd5c0
@ -4,7 +4,6 @@ module.exports = {
|
|||||||
secret : process.env.SECRET || 'test',
|
secret : process.env.SECRET || 'test',
|
||||||
devToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImNsaWVudCI6InRlc3QiLCJjbGllbnRfdHlwZSI6InNpdGUiLCJ1c2VyX3R5cGUiOiJ1c2VyIiwidXNlcl9pZCI6MjAwLCJjaGFubmVsIjoidGVzdF9jaGFubmVsIn0sImF1ZCI6ImludGVybmFsIiwiaXNzIjoiWWFyZHN0aWNrIFNvZnR3YXJlIiwic3ViIjoiQnJhaWQgSldUIn0.5KNCov_EW1cycT4Ay0oSvk4Z4PHFedd3bWOyqkHHTBQ',
|
devToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImNsaWVudCI6InRlc3QiLCJjbGllbnRfdHlwZSI6InNpdGUiLCJ1c2VyX3R5cGUiOiJ1c2VyIiwidXNlcl9pZCI6MjAwLCJjaGFubmVsIjoidGVzdF9jaGFubmVsIn0sImF1ZCI6ImludGVybmFsIiwiaXNzIjoiWWFyZHN0aWNrIFNvZnR3YXJlIiwic3ViIjoiQnJhaWQgSldUIn0.5KNCov_EW1cycT4Ay0oSvk4Z4PHFedd3bWOyqkHHTBQ',
|
||||||
port: process.env.PORT || 80,
|
port: process.env.PORT || 80,
|
||||||
httpPort: process.env.HTTP_PORT || 8080,
|
|
||||||
hostname: process.env.HOSTNAME || 'ysbraid.localhost',
|
hostname: process.env.HOSTNAME || 'ysbraid.localhost',
|
||||||
environment: process.env.ENVIRONMENT || 'development',
|
environment: process.env.ENVIRONMENT || 'development',
|
||||||
log_level: process.env.LOG_LEVEL || 'debug',
|
log_level: process.env.LOG_LEVEL || 'debug',
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
|
import * as express from 'express';
|
||||||
|
|
||||||
// internal imports
|
// internal imports
|
||||||
const app = require('./config/app');
|
const app = require('./config/app');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
import ClientManager from './clientManager';
|
import ClientManager from './clientManager';
|
||||||
import ChannelManager from './channelManager';
|
import ChannelManager from './channelManager';
|
||||||
@ -14,10 +16,15 @@ import PublicClient from './clients/types/publicClient';
|
|||||||
import PrivateClient from './clients/types/privateClient';
|
import PrivateClient from './clients/types/privateClient';
|
||||||
import CustomClient from './clients/types/customClient';
|
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 clientManager = new ClientManager();
|
||||||
const channelManager = new ChannelManager();
|
const channelManager = new ChannelManager();
|
||||||
const xhrListener = new XhrListener(channelManager);
|
const xhrListener = new XhrListener(channelManager, server, appExpress, express.json({type: 'application/json'}));
|
||||||
|
|
||||||
function connectionManager() {
|
function connectionManager() {
|
||||||
wss.on('connection', (ws: WebSocket, request: any, args: string) => {
|
wss.on('connection', (ws: WebSocket, request: any, args: string) => {
|
||||||
|
@ -5,9 +5,10 @@ const url = require('url');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const app = require('./config/app');
|
const app = require('./config/app');
|
||||||
|
const cors = require('cors');
|
||||||
|
|
||||||
const AUTH_TOKEN = app.archimedesToken;
|
const AUTH_TOKEN = app.archimedesToken;
|
||||||
const HTTP_PORT = app.httpPort;
|
const HTTP_PORT = app.port;
|
||||||
|
|
||||||
import { Server, IncomingMessage, ServerResponse } from 'http'
|
import { Server, IncomingMessage, ServerResponse } from 'http'
|
||||||
|
|
||||||
@ -19,14 +20,55 @@ interface ArchimedesMessage {
|
|||||||
class XhrListener {
|
class XhrListener {
|
||||||
#cm: ChannelManager;
|
#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
|
// 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
|
// callback and event handlers out into class methods. It will be
|
||||||
// problematic if the actual `this` needs to be accessed.
|
// problematic if the actual `this` needs to be accessed.
|
||||||
let xhrlisten = http.createServer(this.onRequest.bind(this));
|
|
||||||
xhrlisten.listen(HTTP_PORT);
|
app.post('/event/resume', cors(),
|
||||||
this.#cm = cm;
|
(req: any, res: any, next: any) => this.eventFromArchimedes.bind(this)("resumeExam", req, res));
|
||||||
logger.debugLog.info("XhrListener running");
|
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 {
|
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
|
// did not match a channel; examId isn't valid for some reason
|
||||||
return false;
|
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;
|
export default XhrListener;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user