implement XhrListener to forward POSTs to WS
This commit is contained in:
@ -7,27 +7,42 @@ const logPath = './logs/';
|
|||||||
const tsFormat = () => (new Date().toISOString());
|
const tsFormat = () => (new Date().toISOString());
|
||||||
const logFormat = format.combine(format.timestamp(), format.json());
|
const logFormat = format.combine(format.timestamp(), format.json());
|
||||||
|
|
||||||
function loggerTransports(logName: string, logLevel: string) {
|
function loggerTransports(logLevel: string, logName = "") {
|
||||||
return [
|
let transport:any;
|
||||||
new transports.File({
|
if ("" === logName) {
|
||||||
|
transport = new transports.Console({
|
||||||
|
format: format.simple(),
|
||||||
|
level: logLevel,
|
||||||
|
timestamp: tsFormat
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
transport = new transports.File({
|
||||||
filename: path.join(logPath, `${logName}.log`),
|
filename: path.join(logPath, `${logName}.log`),
|
||||||
timestamp: tsFormat,
|
timestamp: tsFormat,
|
||||||
level: logLevel
|
level: logLevel
|
||||||
})
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return [ transport ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debugLog = createLogger({
|
||||||
|
format: format.simple(),
|
||||||
|
transports: loggerTransports('debug')
|
||||||
|
});
|
||||||
|
|
||||||
const errorLog = createLogger({
|
const errorLog = createLogger({
|
||||||
format: logFormat,
|
format: logFormat,
|
||||||
transports: loggerTransports('errors', 'debug')
|
transports: loggerTransports('debug', 'errors')
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessLog = createLogger({
|
const accessLog = createLogger({
|
||||||
format: logFormat,
|
format: logFormat,
|
||||||
transports: loggerTransports('access', 'info')
|
transports: loggerTransports('info', 'access')
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
errorLog,
|
errorLog,
|
||||||
accessLog
|
accessLog,
|
||||||
|
debugLog
|
||||||
};
|
};
|
||||||
|
@ -9,6 +9,7 @@ const logger = require('./logger');
|
|||||||
|
|
||||||
import ClientManager from './clientManager';
|
import ClientManager from './clientManager';
|
||||||
import ChannelManager from './channelManager';
|
import ChannelManager from './channelManager';
|
||||||
|
import XhrListener from './xhrListener';
|
||||||
import PublicClient from './clients/types/publicClient';
|
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';
|
||||||
@ -16,6 +17,7 @@ import CustomClient from './clients/types/customClient';
|
|||||||
const wss = new WebSocket.Server({ maxPayload: 250000, port: app.port });
|
const wss = new WebSocket.Server({ maxPayload: 250000, port: app.port });
|
||||||
const clientManager = new ClientManager();
|
const clientManager = new ClientManager();
|
||||||
const channelManager = new ChannelManager();
|
const channelManager = new ChannelManager();
|
||||||
|
const xhrListener = new XhrListener(channelManager);
|
||||||
|
|
||||||
function connectionManager() {
|
function connectionManager() {
|
||||||
wss.on('connection', (ws: WebSocket, request: any, args: string) => {
|
wss.on('connection', (ws: WebSocket, request: any, args: string) => {
|
||||||
@ -63,7 +65,7 @@ function connectionManager() {
|
|||||||
|
|
||||||
channelManager.purgeEmptyChannels();
|
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 = {
|
module.exports = {
|
||||||
clientManager,
|
clientManager,
|
||||||
channelManager,
|
channelManager,
|
||||||
connectionManager
|
connectionManager,
|
||||||
|
xhrListener
|
||||||
};
|
};
|
||||||
|
120
src/xhrListener.ts
Normal file
120
src/xhrListener.ts
Normal file
@ -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;
|
Reference in New Issue
Block a user