Skip to content

Instantly share code, notes, and snippets.

@azasypkin
Last active August 29, 2015 14:11
Show Gist options
  • Save azasypkin/e4e99bda58ca2b4436c2 to your computer and use it in GitHub Desktop.
Save azasypkin/e4e99bda58ca2b4436c2 to your computer and use it in GitHub Desktop.
FxOS WebSocketServer draft
/* exported WebSocketUtils */
(function(exports) {
'use strict';
exports.WebSocketUtils = {
/**
* Mask every data element with the mask (WebSocket specific algorithm).
* @param {Array} mask Mask array.
* @param {Array} array Data array to mask.
* @returns {Array} Masked data array.
*/
mask: function (mask, array) {
if (mask) {
for (var i = 0; i < array.length; i++) {
array[i] = array[i] ^ mask[i % 4];
}
}
return array;
},
/**
* Generates 4-item array, every item of which is element of byte mask.
* @returns {Array}
*/
generateRandomMask: function () {
return [
~~(Math.random() * 255),
~~(Math.random() * 255),
~~(Math.random() * 255),
~~(Math.random() * 255)
];
},
/**
* Converts string to Uint8Array.
* @param {string} stringValue String value to convert.
* @returns {Uint8Array}
*/
stringToArray: function (stringValue) {
if (typeof stringValue !== 'string') {
throw new Error('stringValue should be valid string!');
}
var array = new Uint8Array(stringValue.length);
for (var i = 0; i < stringValue.length; i++) {
array[i] = stringValue.charCodeAt(i);
}
return array;
},
/**
* Converts array to string. Every array element is considered as char code.
* @param {Uint8Array} array Array with the char codes.
* @returns {string}
*/
arrayToString: function (array) {
return String.fromCharCode.apply(null, array);
},
/**
* Reads unsigned 16 bit value from two consequent 8-bit array elements.
* @param {Uint8Array} array Array to read from.
* @param {Number} offset Index to start read value.
* @returns {Number}
*/
readUInt16: function (array, offset) {
return (array[offset] << 8) + array[offset + 1];
},
/**
* Reads unsigned 32 bit value from four consequent 8-bit array elements.
* @param {Uint8Array} array Array to read from.
* @param {Number} offset Index to start read value.
* @returns {Number}
*/
readUInt32: function (array, offset) {
return (array[offset] << 24) +
(array[offset + 1] << 16) +
(array [offset + 2] << 8) +
array[offset + 3];
},
/**
* Writes unsigned 16 bit value to two consequent 8-bit array elements.
* @param {Uint8Array} array Array to write to.
* @param {Number} value 16 bit unsigned value to write into array.
* @param {Number} offset Index to start write value.
* @returns {Number}
*/
writeUInt16: function (array, value, offset) {
array[offset] = (value & 0xff00) >> 8;
array[offset + 1] = value & 0xff;
},
/**
* Writes unsigned 16 bit value to two consequent 8-bit array elements.
* @param {Uint8Array} array Array to write to.
* @param {Number} value 16 bit unsigned value to write into array.
* @param {Number} offset Index to start write value.
* @returns {Number}
*/
writeUInt32: function (array, value, offset) {
array[offset] = (value & 0xff000000) >> 24;
array[offset + 1] = (value & 0xff0000) >> 16;
array[offset + 2] = (value & 0xff00) >> 8;
array[offset + 3] = value & 0xff;
}
};
})(window);
/* global CryptoJS,
EventDispatcher,
Map,
TCPSocketEvent,
WeakMap,
WebSocketUtils
*/
(function(exports) {
'use strict';
/**
* Sequence used to separate HTTP request headers and body.
* @const {string}
*/
const CRLF = '\r\n';
/**
* Magic GUID defined by RFC to concatenate with web socket key during
* websocket handshake.
* @const {string}
*/
const WEBSOCKET_KEY_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
/**
* Websocket handshake response template string, {web-socket-key} should be
* replaced with the appropriate key.
* @const {string}
*/
const WEBSOCKET_HANDSHAKE_RESPONSE =
'HTTP/1.1 101 Switching Protocols' + CRLF +
'Connection: Upgrade' + CRLF +
'Upgrade: websocket' + CRLF +
'Sec-WebSocket-Accept: {web-socket-key}' + CRLF + CRLF;
/**
* Enumeration of all possible operation codes.
* @enum {number}
*/
const OperationCode = {
CONTINUATION_FRAME: 0,
TEXT_FRAME: 1,
BINARY_FRAME: 2,
CONNECTION_CLOSE: 8,
PING: 9,
PONG: 10
};
/**
* Map used to store private members for every WebSocket instance.
* @type {WeakMap}
*/
var priv = new WeakMap();
/**
* Extracts HTTP header map from HTTP header string.
* @param {string} httpHeaderString HTTP header string.
* @returns {Map.<string, string>} HTTP header key-value map.
*/
function getHttpHeaders(httpHeaderString) {
var httpHeaders = httpHeaderString.trim().split(CRLF);
return new Map(httpHeaders.map((httpHeader) => {
return httpHeader.split(':').map((entity) => entity.trim());
}));
}
/**
* Performs WebSocket HTTP Handshake.
* @param {TCPSocket} socket Connection socket.
* @param {Uint8Array} httpRequestData HTTP Handshake data array.
* @returns {Map.<string, string>} Parsed http headers
*/
function performHandshake(socket, httpRequestData) {
var httpHeaders = getHttpHeaders(
WebSocketUtils.arrayToString(httpRequestData).split(CRLF + CRLF)[0]
);
var webSocketKey = CryptoJS.SHA1(
httpHeaders.get('Sec-WebSocket-Key') + WEBSOCKET_KEY_GUID
).toString(CryptoJS.enc.Base64);
var arrayResponse = WebSocketUtils.stringToArray(
WEBSOCKET_HANDSHAKE_RESPONSE.replace('{web-socket-key}', webSocketKey)
);
socket.send(arrayResponse.buffer, 0, arrayResponse.byteLength);
return httpHeaders;
}
/**
* MozTcpSocket data handler.
* @param {TCPSocketEvent} eData TCPSocket data event.
*/
function onSocketData(eData) {
/* jshint validthis: true */
var members = priv.get(this);
var frameData = new Uint8Array(eData.data);
// If we don't have connection info from this host let's perform handshake
// Currently we support only ONE client from host.
if (!members.clients.has(members.socket.host)) {
var handshakeResult = performHandshake(members.socket, frameData);
if (handshakeResult) {
members.clients.set(members.socket.host, handshakeResult);
}
return;
}
members.onMessageFrame(frameData);
}
/**
* Process WebSocket incoming frame.
* @param {Uint8Array} frame Message frame data in view of Uint8Array.
*/
function onMessageFrame(frame) {
/* jshint validthis: true */
var state = {
isCompleted: (frame[0] & 0x80) == 0x80,
isMasked: (frame[1] & 0x80) == 0x80,
isCompressed: (frame[0] & 0x40) == 0x40,
opCode: frame[0] & 0xf,
dataLength: frame[1] & 0x7f,
frame: frame,
data: null,
mask: null
};
if (state.opCode === OperationCode.CONTINUATION_FRAME) {
console.error('Continuation frame is not yet supported!');
return;
}
if (state.opCode === OperationCode.PING) {
console.error('Ping frame is not yet supported!');
return;
}
if (state.opCode === OperationCode.PONG) {
console.error('Pong frame is not yet supported!');
return;
}
if (state.opCode === OperationCode.CONNECTION_CLOSE) {
console.error('Close frame is not yet supported!');
return;
}
var maskAndDataOffset = 2;
if (state.dataLength === 126) {
state.dataLength = WebSocketUtils.readUInt16(frame, 2);
maskAndDataOffset += 2;
} else if (state.dataLength == 127) {
state.dataLength = WebSocketUtils.readUInt32(frame, 2);
maskAndDataOffset += 4;
}
if (state.isMasked) {
// Get 32 bits for masking key
state.mask = state.frame.subarray(
maskAndDataOffset, maskAndDataOffset + 4
);
state.data = state.frame.subarray(maskAndDataOffset + 4);
} else {
state.data = state.frame.subarray(maskAndDataOffset);
}
if (state.opCode === OperationCode.TEXT_FRAME ||
state.opCode === OperationCode.BINARY_FRAME) {
this.emit('message', WebSocketUtils.mask(state.mask, state.data));
}
}
/**
* Creates outgoing websocket message frame.
* @param {Number} opCode Frame operation code.
* @param {Uint8Array} data Data array.
* @param {Boolean} isComplete Indicates if frame is completed.
* @param {Boolean} isMasked Indicates if frame data should be masked.
* @returns {Uint8Array} Constructed frame data.
*/
function createMessageFrame(opCode, data, isComplete, isMasked) {
var dataLength = data.length;
var dataOffset = isMasked ? 6 : 2;
var secondByte = 0;
if (dataLength >= 65536) {
dataOffset += 8;
secondByte = 127;
} else if (dataLength > 125) {
dataOffset += 2;
secondByte = 126;
} else {
secondByte = dataLength;
}
var outputBuffer = new Uint8Array(dataOffset + dataLength);
// Writing OPCODE, FIN and LENGTH
outputBuffer[0] = isComplete ? opCode | 0x80 : opCode;
outputBuffer[1] = isMasked ? secondByte | 0x80 : secondByte;
// Writing DATA LENGTH
switch (secondByte) {
case 126:
WebSocketUtils.writeUInt16(outputBuffer, dataLength, 2);
break;
case 127:
WebSocketUtils.writeUInt32(outputBuffer, 0, 2);
WebSocketUtils.writeUInt32(outputBuffer, dataLength, 6);
break;
}
if (isMasked) {
var mask = WebSocketUtils.generateRandomMask();
// Writing MASK
outputBuffer[dataOffset - 4] = mask[0];
outputBuffer[dataOffset - 3] = mask[1];
outputBuffer[dataOffset - 2] = mask[2];
outputBuffer[dataOffset - 1] = mask[3];
WebSocketUtils.mask(mask, data);
}
for(var i = 0; i < data.length; i++) {
outputBuffer[dataOffset + i] = data[i];
}
return outputBuffer;
}
/**
* WebSocketServer constructor that accepts port to listen on.
* @param {Number} port Number to listen for websocket connections.
* @constructor
*/
var WebSocketServer = function(port) {
EventDispatcher.mixin(this, ['message']);
var privateMembers = {
tcpSocket: navigator.mozTCPSocket.listen(port, {
binaryType: 'arraybuffer'
}),
clients: new Map(),
// Private methods
onSocketData: onSocketData.bind(this),
onMessageFrame: onMessageFrame.bind(this)
};
privateMembers.tcpSocket.onconnect = (eSocket) => {
eSocket.ondata = privateMembers.onSocketData;
eSocket.onclose = () => privateMembers.clients.delete(eSocket.host);
eSocket.onerror = () => privateMembers.clients.delete(eSocket.host);
privateMembers.socket = eSocket;
};
priv.set(this, privateMembers);
};
/**
* Send data to the connected client
* @param {ArrayBuffer|Array|string} data Data to send.
*/
WebSocketServer.prototype.send = function(data) {
if (!ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
if (typeof data === 'string') {
data = new Uint8Array(WebSocketUtils.stringToArray(data));
} else if (Array.isArray(data)) {
data = new Uint8Array(data);
} else {
throw new Error('Unsupported data type: ' + typeof data);
}
}
var dataFrame = createMessageFrame(0x2, data, true, false);
priv.get(this).socket.send(dataFrame.buffer, 0, dataFrame.length);
};
exports.WebSocketServer = WebSocketServer;
})(window);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment