Last active
August 29, 2015 14:11
-
-
Save azasypkin/e4e99bda58ca2b4436c2 to your computer and use it in GitHub Desktop.
FxOS WebSocketServer draft
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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