// James Taylor - 466598 // Framing module - Decodes and packages data function framer() { var connections = require("./connections"); // used for the server to emit events var emitter = ""; var connection; // Ensures the full length of the frame has been recieved this.checkRecievedData = function(data, connectionID) { // checks to see if more frames are expected if (connection.returnWaitState(connectionID) == true) { connection.addWaitArray(data, connectionID); var concatenatedArray = connection.returnWaitArray(connectionID); // Set length of expected data if ((concatenatedArray[1] & 127) <= 125) { var setLength = (concatenatedArray[1] & 127) + 6; connection.addFullWaitLength(connectionID, setLength); } if ((concatenatedArray[1] & 127) == 126) { if (concatenatedArray.length > 4) { var setLength = returnLength(concatenatedArray); connection.addFullWaitLength(connectionID, setLength); } } if ((concatenatedArray[1] & 127) == 127) { if (concatenatedArray.length > 10) { var setLength = returnLength(concatenatedArray); connection.addFullWaitLength(connectionID, setLength); } } if (concatenatedArray.length == connection.returnFullWaitLength(connectionID)) { // Reset wait variables connection.changeWaitState(connectionID, false); connection.clearWaitArray(connectionID); connection.clearWaitLength(connectionID); // Pass on message for decoding decode(concatenatedArray, connectionID); } } else { if (data[0] == 129 || data[0] == 130 || data[0] == 136 || data[0] == 137 || data[0] == 138) { if (data[0] == 138) { function splitFrames(data) { var checkLength = returnLength(data); if (data.length > checkLength) { var newFragment = data.slice(0, checkLength); decode(newFragment, connectionID); var unhandledFragment = data.slice(checkLength); splitFrames(unhandledFragment); } else { checkFullFrameRecieved(data, connectionID); decode(data, connectionID); } } splitFrames(data); } else if (data[0] == 136) { decode(data, connectionID); return; } else { checkFullFrameRecieved(data,connectionID); } if (connection.returnWaitState(connectionID) == false) { decode(data, connectionID); } else { connection.addWaitArray(data, connectionID); } } else if (data[0] == 1 || data[0] == 2 || data[0] == 128 || data[0] == 0) { // fragmentation // if continuation frame is sent with no message to continue if (data[0] == 0 && connection.fragmentArray("get", data, connectionID).length == 0) { connection.closeConnection(connectionID, 1002); } // if continuation frame is sent with its FIN set but it has no message to continue else if (data[0] == 128 && connection.fragmentArray("get", data, connectionID).length == 0) { connection.closeConnection(connectionID, 1002); } else if (data.length < 6) { connection.changeWaitState(connectionID, true); connection.addWaitArray(data, connectionID); } else { function checkFragments(data) { //check frame is fragmented, if not divide it var checkLength = returnLength(data); if (data.length > checkLength) { var newFragment = data.slice(0, checkLength); handleFragmentation(newFragment, connectionID); var unhandledFragment = data.slice(checkLength); checkFragments(unhandledFragment); } else { handleFragmentation(data, connectionID); } } checkFragments(data); } } else { connection.closeConnection(connectionID, 1002); } } } // Handles fragmentation cases function handleFragmentation(data, connectionID) { var opcode = data[0]; // Pings may be sent in the middle of fragments if (opcode == 137) { decode(data, connectionID); return; } else if (opcode == 136) { decode(data, connectionID); return; } if (opcode == 0 && connection.fragmentArray("get", data, connectionID).length == 0) { connection.closeConnection(connectionID, 1002); } else if ((opcode == 1 || opcode == 2 || opcode == 129) && (connection.fragmentArray("get", data, connectionID).length != 0)) { connection.closeConnection(connectionID, 1002); } // remember the opcode for sending later if (opcode == 1) { connection.fragmentOpcode("set", 129, connectionID); } else if (opcode == 2) { connection.fragmentOpcode("set", 130, connectionID); } // unmask the message var maskLocation = 2; var dataLength = data[1] & 127; if (dataLength == 126) { maskLocation = 4; } else if (dataLength == 127) { maskLocation = 10; } var unmaskedData = []; var maskingKey = data.slice(maskLocation, maskLocation + 4); var dataLocation = maskLocation + 4; for (var i = 0; dataLocation < data.length; i++) { var newData = data[dataLocation] ^ maskingKey[i % 4]; unmaskedData.push(newData); dataLocation++; } var buf = new Buffer(unmaskedData); connection.fragmentArray("set", buf, connectionID); // Message with FIN set to 1, package fragments for sending if (opcode == 128) { var dataBuffer = connection.fragmentArray("get", buf, connectionID); connection.fragmentArray("reset", buf, connectionID); opcode = connection.fragmentOpcode("get", 0, connectionID); // Construct the first bytes of the message var newData = []; newData[0] = opcode; setPayloadLength(newData, dataBuffer.length); var buf = new Buffer(newData); var bufList = []; bufList[0] = buf; bufList[1] = dataBuffer; var packagedMessage = Buffer.concat(bufList); connection.sendFrame(packagedMessage, connectionID); } } // Used to correctly handle the data recieved from the client function decode(data, connectionID) { // The type of data (Opcode) var opcode = data[0]; // The length of the data must be greater than 5 to be a valid frame if (data.length <= 5) { connection.closeConnection(connectionID, 1008); console.log("Frame Length too short!"); return; } // make the initial check on the RSV numbers if (invalidRSV(opcode)) { connection.closeConnection(connectionID, 1002); } if (opcode == 129) { // text message emitter.emit("message", data, connectionID); var decodedData = unmaskMessage(data); var packagedData = packageData(decodedData); var encodedBuffer = new Buffer(packagedData); connection.sendFrame(encodedBuffer, connectionID); } else if (opcode == 1 || opcode == 2 || opcode == 0 || opcode == 128) { // fragmented messages handleFragmentation(data, connectionID); } else if (opcode == 130) { // binary frame emitter.emit("message", data, connectionID); var decodedData = unmaskMessage(data); var packagedData = packageData(decodedData); var encodedBuffer = new Buffer(packagedData); connection.sendFrame(encodedBuffer, connectionID); } else if (opcode == 136) { // close frame // Check for valid close code and close the connection validateCloseFrame(connectionID, data); } else if (opcode == 137) { // ping frame if (data.length <= 131) { var decodedData = unmaskMessage(data); var packagedData = constructPongFrame(decodedData); var encodedBuffer = new Buffer(packagedData); connection.sendFrame(encodedBuffer, connectionID); } else { connection.closeConnection(connectionID, 1002); } } else if (opcode == 138) { // pong frame console.log("Pong recieved"); } else { connection.closeConnection(connectionID, 1002); } } // Used to package the data in the correct framing format for WebSockets function packageData(data) { // extract the opcode from the data array then remove it from the array var opcode = data.pop(); var newData = []; newData[0] = opcode; var payloadLength = data.length; if (payloadLength <= 125) { var dataLocation = 2; newData[1] = payloadLength; } else if (payloadLength >= 126 && payloadLength <= 65535) { var dataLocation = 4; newData[1] = 126; newData[2] = payloadLength >>> 8; newData[3] = payloadLength & 255; } else if (payloadLength >= 65536) { var dataLocation = 10; newData[1] = 127; // The first 32 bits newData[9] = (payloadLength) & 255; newData[8] = (payloadLength >> 8) & 255; newData[7] = (payloadLength >> 16) & 255; newData[6] = (payloadLength >> 24) & 255; // if the number is greater than 32bit if (payloadLength >= 4294967296) { // Get the higher 64 bit var sixtyFourBit = payloadLength.toString(2); var thirtyTwoBit = sixtyFourBit.slice(0, (sixtyFourBit.length - 32)); var secondThirtyTwoBits = parseInt(thirtyTwoBit, 2); newData[5] = (secondThirtyTwoBits) & 255; newData[4] = (secondThirtyTwoBits >> 8) & 255; newData[3] = (secondThirtyTwoBits >> 16) & 255; newData[2] = (secondThirtyTwoBits >> 24) & 255; } else { // if number is less than 32bit newData[5] = 0; newData[4] = 0; newData[3] = 0; newData[2] = 0; } } for (var i = 0; i < payloadLength; i++) { newData[dataLocation] = data[i]; dataLocation++; } return newData; } // unmasks a message function unmaskMessage(data) { // The payload length var payloadLength = data[1]; // The location of the masking key // Defualt 2 var locationOfMask = 2; // Find the length of the message being sent, remove the masking key using & 127 var payloadLength = payloadLength & 127; if (payloadLength == 126) { // If the length of the payload is 126, 2 extra bytes represent the length locationOfMask = 4; } else if (payloadLength == 127) { // If length of the payload is 127, 8 extra bytes represent the length locationOfMask = 10; } // Mask is 4 bytes long var maskingKey = data.slice(locationOfMask, locationOfMask + 4); var dataLocation = locationOfMask + 4; // new array to hold decodedData var decodedData = []; var logString = ""; for (var i = 0; dataLocation < data.length; i++) { var newData = data[dataLocation] ^ maskingKey[i % 4]; decodedData.push(newData); dataLocation++; } decodedData.push(data[0]); return decodedData; } // Used to set the correct payload length for packaging data function setPayloadLength(newData, payloadLength) { if (payloadLength <= 125) { var dataLocation = 2; newData[1] = payloadLength; } else if (payloadLength >= 126 && payloadLength <= 65535) { var dataLocation = 4; newData[1] = 126; newData[2] = payloadLength >>> 8; newData[3] = payloadLength & 255; } else if (payloadLength >= 65536) { var dataLocation = 10; newData[1] = 127; // The first 32 bits newData[9] = (payloadLength) & 255; newData[8] = (payloadLength >> 8) & 255; newData[7] = (payloadLength >> 16) & 255; newData[6] = (payloadLength >> 24) & 255; // if the number is greater than 32bit if (payloadLength >= 4294967296) { var sixtyFourBit = payloadLength.toString(2); var thirtyTwoBit = sixtyFourBit.slice(0, (sixtyFourBit.length - 32)); var secondThirtyTwoBits = parseInt(thirtyTwoBit, 2); newData[5] = (secondThirtyTwoBits) & 255; newData[4] = (secondThirtyTwoBits >> 8) & 255; newData[3] = (secondThirtyTwoBits >> 16) & 255; newData[2] = (secondThirtyTwoBits >> 24) & 255; } else { // if number is less than 32bit newData[5] = 0; newData[4] = 0; newData[3] = 0; newData[2] = 0; } } } // Creates a Pong frame function constructPongFrame(data) { var pingOpcode = data.pop(); var newData = []; newData[0] = 138; newData[1] = data.length; var dataLocation = 2; var i = 0; while (i < data.length) { newData[dataLocation] = data[i]; dataLocation++; i++; } return newData; } // Used to check the full frame has been recieved function checkFullFrameRecieved(data, connectionID) { var payloadLength = data[1] & 127; //remove mask bit if (payloadLength <= 125) { if ((data.length - 6) == payloadLength) { //console.log("Correct Frame Size"); } else { connection.changeWaitState(connectionID, true); } } else if (payloadLength == 126) { var dataRecieved = data.slice(8, (data.length)); var payOne = data[2] << 8; var payTwo = data[3]; var dataLength = payOne + payTwo; if (dataLength == dataRecieved.length) { //console.log("Correct Frame Size"); } else { connection.changeWaitState(connectionID, true); } } else if (payloadLength == 127) { var dataRecieved = data.slice(14, (data.length)); var dataLength = ""; for (var i = 2; i < 10; i++) { if (data[i] == 0) { var str ="00000000"; } else { var newByte = data[i]; var str = newByte.toString(2); } dataLength += str; } dataLength = parseInt(dataLength, 2); if (dataLength == dataRecieved.length) { //console.log("Frame Recieved"); } else { connection.changeWaitState(connectionID, true); } } } // Returns the expected length of the data as specified by the Payload Length byte (2nd byte); function returnLength(data) { if ((data[1] & 127) <= 125) { return (data[1] & 127) + 6; } else if ((data[1] & 127) == 126) { var dataLength = (data[2] << 8) + data[3]; return dataLength + 8; } else if ((data[1] & 127) == 127) { var dataLength = ""; for (var i = 2; i < 10; i++) { if (data[i] == 0) { var str ="00000000"; } else { var newByte = data[i]; var str = newByte.toString(2); } dataLength += str; } dataLength = parseInt(dataLength, 2); return dataLength + 14; } } // Checks that RSV1, RSV2 and RSV3 are all set to 0 function invalidRSV(firstByte) { var check = firstByte; // remove opcode and FIN; check = check >> 4; check = check & 7; if (check == 0) { return false; } else { return true; } } // Validates a close frame function validateCloseFrame(connectionID, data) { if (data.length == 6) { // No close code connection.closeConnection(connectionID, 1000); return; } else if (data.length == 7) { // Only half close code connection.closeConnection(connectionID, 1002); return; } // CLOSE CODE CHECKING var mask = data.slice(2, 6); var maskedCode = data.slice(6, 8); var unmaskedCode = []; for (var i = 0; i < maskedCode.length; i++) { unmaskedCode[i] = maskedCode[i] ^ mask[i % 4]; } unmaskedCode[0] = unmaskedCode[0] << 8; var statusCode = unmaskedCode[0] + unmaskedCode[1]; if (statusCode <= 999 || statusCode >= 5000) { connection.closeConnection(connectionID, 1002); } else if (statusCode >= 3000 && statusCode <= 4999) { connection.closeConnection(connectionID, 1000); } else { switch(statusCode) { case 1000: connection.closeConnection(connectionID, 1000); break; case 1001: connection.closeConnection(connectionID, 1000); break; case 1002: connection.closeConnection(connectionID, 1000); break; case 1003: connection.closeConnection(connectionID, 1000); break; case 1007: connection.closeConnection(connectionID, 1000); break; case 1008: connection.closeConnection(connectionID, 1000); break; case 1009: connection.closeConnection(connectionID, 1000); break; case 1010: connection.closeConnection(connectionID, 1000); break; case 1011: connection.closeConnection(connectionID, 1000); break; default: connection.closeConnection(connectionID, 1002); } } } this.setEmitter = function(object) { emitter = object; } this.setConnectionList = function(list) { connection = list; } this.unmaskMessageCaller = function(message) { return unmaskMessage(message); } this.packageDataCaller = function(data) { return packageData(data); } } module.exports = framer;