Merge pull request #14 from yardstick/feature/QUANT-359-archimedes-integration-build-cors-compliant-web-server
QUANT-359 implement XhrListener to forward POSTs to WS
This commit is contained in:
commit
ce2c65544c
6
Jenkinsfile
vendored
6
Jenkinsfile
vendored
@ -12,12 +12,12 @@ 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') {
|
||||
container('base') {
|
||||
sh "tsc"
|
||||
sh "npx tsc"
|
||||
}
|
||||
}
|
||||
stage('Run unit tests') {
|
||||
@ -44,4 +44,4 @@ podTemplate(label: label, inheritFrom: 'base', , containers: [
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# BRAID v1.2.3
|
||||
# BRAID v1.3.0
|
||||
> Websocket server for the Measure platform
|
||||
|
||||
[](https://semaphoreci.com/yardstick/braid)
|
||||
|
@ -1 +1 @@
|
||||
1.2.3
|
||||
1.3.0
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -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',
|
||||
@ -15,5 +15,6 @@ module.exports = {
|
||||
audience: 'internal',
|
||||
algorithm: ['HS256']
|
||||
},
|
||||
messageTypes : ['broadcast', 'direct', 'changeChannel']
|
||||
messageTypes : ['broadcast', 'direct', 'changeChannel'],
|
||||
archimedesToken: process.env.ARCHIMEDES_INTEGRATION_TOKEN || 'testkeyformeasure'
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -2,20 +2,29 @@
|
||||
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';
|
||||
import XhrListener from './xhrListener';
|
||||
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, server, appExpress, express.json({type: 'application/json'}));
|
||||
|
||||
function connectionManager() {
|
||||
wss.on('connection', (ws: WebSocket, request: any, args: string) => {
|
||||
@ -63,7 +72,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 +97,6 @@ startServer();
|
||||
module.exports = {
|
||||
clientManager,
|
||||
channelManager,
|
||||
connectionManager
|
||||
connectionManager,
|
||||
xhrListener
|
||||
};
|
||||
|
104
src/xhrListener.ts
Normal file
104
src/xhrListener.ts
Normal file
@ -0,0 +1,104 @@
|
||||
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 cors = require('cors');
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
class XhrListener {
|
||||
#cm: ChannelManager;
|
||||
|
||||
constructor(cm: ChannelManager, server: any, app: any, jsonParser: any) {
|
||||
|
||||
this.#cm = cm;
|
||||
logger.debugLog.info("XhrListener running");
|
||||
|
||||
//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());
|
||||
|
||||
// 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.
|
||||
|
||||
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, 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'], req.rawBody);
|
||||
|
||||
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 {
|
||||
logger.debugLog.info(`XhrListener:relayEvent(event: ${event}, channel: ${archMsg.reservation_no})`);
|
||||
for (let c of this.#cm.channels) {
|
||||
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({
|
||||
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.
|
||||
return dmCount > 0;
|
||||
}
|
||||
}
|
||||
// did not match a channel; examId isn't valid for some reason
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default XhrListener;
|
Loading…
x
Reference in New Issue
Block a user