/**
* The Rodney for Gabbo external request server.
*
* @module server
* @requires module:net
* @requires module:md5
* @requires module:github
* @requires module:gitlab
* @requires module:bitbucket
*/
var net = require('net');
var md5 = require('md5');
var github = require('./github.js');
var gitlab = require('./gitlab.js');
var bitbucket = require('./bitbucket.js');
const PORT = 2080;
const HOST = 'localhost';
//var HOST = 'mud.panterasbox.com';
const SIZE_WIDTH = 4;
const MD5_WIDTH = 32;
const ENCODING = 'utf8'
startServer();
/**
* Start up the Rodney for Gabbo server.
*
* @function
* @return {object} the newly started server instance
*/
function startServer() {
var server = net.createServer(function(socket) {
var state = { };
resetState(state);
socket.on('data', function(data) {
if (handleRequest(data, state, socket)) {
resetState(state);
}
});
});
server.listen(PORT, HOST);
return server;
}
/**
* Reset the current request state to its starting values. This state object
* will read the headers in the first request object, then append subsequent
* requests to a message buffer until the entire request has been received.
* At that time the message will be passed off to handleRequest and the
* request state should be reset.
*
* @function
* @param {object} state - processing state of current request
* @param {object} state.buffer - the Buffer containing partial message
* received so far
* @param {number} state.cursor - the current position in the message
* @param {number} state.size - the total size of the message
* @param {string} state.checksum - the md5 checksum of the message
*/
function resetState(state) {
state.buffer = null;
state.cursor = -1;
state.size = 0;
state.checksum = 0;
}
/**
* Handle an incoming request. A single message may span multiple requests,
* and the first request for a message will include size/checksum headers
* in addition to the message being sent. The existing message state is
* maintained by the calling scope and must be passed as a paramter.
*
* @function
* @param {object} data - the Buffer containing incoming data
* @param {object} st - processing state of current request
* @param {object} socket - the server socket for sending responses
* @return {boolean} true if message was completed, false to keep going
*/
function handleRequest(data, st, socket) {
var pos = 0;
if (st.buffer == null) {
st.size += (data[0] & 0xFF) * 0x1000000;
st.size += (data[1] & 0xFF) * 0x10000;
st.size += (data[2] & 0xFF) * 0x100;
st.size += (data[3] & 0xFF);
st.checksum = data.toString(ENCODING, SIZE_WIDTH,
SIZE_WIDTH + MD5_WIDTH);
st.buffer = new Buffer(st.size);
st.cursor = 0;
pos = SIZE_WIDTH + MD5_WIDTH;
console.info("New message: size " + st.size);
}
while (pos < data.length) {
st.buffer[st.cursor++] = data[pos++];
}
if (st.cursor >= st.size) {
if (md5(st.buffer.toString(ENCODING)) != st.checksum) {
console.warn(
"Checksums differ:\n"
+ st.checksum + "\n"
+ md5(st.buffer.toString(ENCODING)) + "\n"
);
} else {
console.info("Got message: size " + st.buffer.length);
var valid = 1;
var incoming;
try {
incoming = JSON.parse(st.buffer);
} catch(err) {
console.error("Illegal request: JSON parse failed");
valid = 0;
}
if (valid && !("transactionId" in incoming)) {
console.error("Illegal request: missing transactionId");
valid = 0;
}
if (valid) {
var transactionId = incoming.transactionId;
var query = incoming.query;
console.info("Got query: " + query +
", transactionId: " + transactionId);
var send = function(data) {
sendResponse(socket, transactionId, data);
};
if (!runQuery(query, send, console.error)) {
console.error("Unsupported operation: " + query);
}
}
return true;
}
}
return false;
}
/**
* Serialize data to JSON and send to client over socket.
*
* @function
* @param {object} socket the socket to send the response over
* @param {string} transactionId the transactionId for this response
* @param {object} data the data to serialize and send
*/
function sendResponse(socket, transactionId, data) {
body = {
transactionId: transactionId,
body: data
};
body = JSON.stringify(body);
body = new Buffer(body); // XXX this sucks
var header = new Buffer(SIZE_WIDTH + MD5_WIDTH);
var size = body.length;
var checksum = md5(body.toString(ENCODING));
header[0] = (size & 0xFFFFFFFF) / 0x1000000;
header[1] = (size & 0xFFFFFF) / 0x10000;
header[2] = (size & 0xFFFF) / 0x100;
header[3] = (size & 0xFF);
header.write(checksum, SIZE_WIDTH, MD5_WIDTH, ENCODING);
console.log("Sending response: size " + size);
socket.write(Buffer.concat([header, body]), ENCODING);
}
/**
* Execute the specified query with onFulfilled and onRejected callbacks.
*
* @param {string} query the query to run
* @param {function} onFulfilled called with result if query is successful
* @param {function} onRjected called with error if query fails
* @return {boolean} true for valid query string, false
* otherwise
*/
function runQuery(query, onFulfilled, onRejected) {
if (query.substring(0, 7) == "github.") {
return github.query(query.substring(7), onFulfilled, onRejected);
} else if (query.substring(0, 7) == "gitlab.") {
return gitlab.query(query.substring(7), onFulfilled, onRejected);
} else if (query.substring(0, 10) == "bitbucket.") {
return bitbucket.query(query.substring(10), onFulfilled, onRejected);
}
return false;
}