356 lines
10 KiB
JavaScript
356 lines
10 KiB
JavaScript
var crypto = require('crypto'),
|
|
Handshake = require('./hybi_parser/handshake'),
|
|
Reader = require('./hybi_parser/stream_reader');
|
|
|
|
var HybiParser = function(webSocket, options) {
|
|
this._reset();
|
|
this._socket = webSocket;
|
|
this._reader = new Reader();
|
|
this._stage = 0;
|
|
this._masking = options && options.masking;
|
|
this._protocols = options && options.protocols;
|
|
|
|
this._pingCallbacks = {};
|
|
|
|
if (typeof this._protocols === 'string')
|
|
this._protocols = this._protocols.split(/\s*,\s*/);
|
|
};
|
|
|
|
HybiParser.mask = function(payload, mask, offset) {
|
|
if (mask.length === 0) return payload;
|
|
offset = offset || 0;
|
|
|
|
for (var i = 0, n = payload.length - offset; i < n; i++) {
|
|
payload[offset + i] = payload[offset + i] ^ mask[i % 4];
|
|
}
|
|
return payload;
|
|
};
|
|
|
|
var instance = {
|
|
BYTE: 255,
|
|
FIN: 128,
|
|
MASK: 128,
|
|
RSV1: 64,
|
|
RSV2: 32,
|
|
RSV3: 16,
|
|
OPCODE: 15,
|
|
LENGTH: 127,
|
|
|
|
OPCODES: {
|
|
continuation: 0,
|
|
text: 1,
|
|
binary: 2,
|
|
close: 8,
|
|
ping: 9,
|
|
pong: 10
|
|
},
|
|
|
|
ERRORS: {
|
|
normal_closure: 1000,
|
|
going_away: 1001,
|
|
protocol_error: 1002,
|
|
unacceptable: 1003,
|
|
encoding_error: 1007,
|
|
policy_violation: 1008,
|
|
too_large: 1009,
|
|
extension_error: 1010,
|
|
unexpected_condition: 1011
|
|
},
|
|
|
|
FRAGMENTED_OPCODES: [0,1,2],
|
|
OPENING_OPCODES: [1,2],
|
|
|
|
ERROR_CODES: [1000,1001,1002,1003,1007,1008,1009,1010,1011],
|
|
|
|
UTF8_MATCH: /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/,
|
|
|
|
getVersion: function() {
|
|
var version = this._socket.request.headers['sec-websocket-version'];
|
|
return 'hybi-' + version;
|
|
},
|
|
|
|
handshakeResponse: function() {
|
|
var secKey = this._socket.request.headers['sec-websocket-key'];
|
|
if (!secKey) return null;
|
|
|
|
var SHA1 = crypto.createHash('sha1');
|
|
SHA1.update(secKey + Handshake.GUID);
|
|
|
|
var accept = SHA1.digest('base64'),
|
|
protos = this._socket.request.headers['sec-websocket-protocol'],
|
|
supported = this._protocols,
|
|
proto,
|
|
|
|
headers = [
|
|
'HTTP/1.1 101 Switching Protocols',
|
|
'Upgrade: websocket',
|
|
'Connection: Upgrade',
|
|
'Sec-WebSocket-Accept: ' + accept
|
|
];
|
|
|
|
if (protos !== undefined && supported !== undefined) {
|
|
if (typeof protos === 'string') protos = protos.split(/\s*,\s*/);
|
|
proto = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
|
|
if (proto) {
|
|
this.protocol = proto;
|
|
headers.push('Sec-WebSocket-Protocol: ' + proto);
|
|
}
|
|
}
|
|
|
|
return new Buffer(headers.concat('','').join('\r\n'), 'utf8');
|
|
},
|
|
|
|
isOpen: function() {
|
|
return true;
|
|
},
|
|
|
|
createHandshake: function(uri) {
|
|
return new Handshake(uri, this._protocols);
|
|
},
|
|
|
|
parse: function(data) {
|
|
this._reader.put(data);
|
|
var buffer = true;
|
|
while (buffer) {
|
|
switch (this._stage) {
|
|
case 0:
|
|
buffer = this._reader.read(1);
|
|
if (buffer) this._parseOpcode(buffer[0]);
|
|
break;
|
|
|
|
case 1:
|
|
buffer = this._reader.read(1);
|
|
if (buffer) this._parseLength(buffer[0]);
|
|
break;
|
|
|
|
case 2:
|
|
buffer = this._reader.read(this._lengthSize);
|
|
if (buffer) this._parseExtendedLength(buffer);
|
|
break;
|
|
|
|
case 3:
|
|
buffer = this._reader.read(4);
|
|
if (buffer) {
|
|
this._mask = buffer;
|
|
this._stage = 4;
|
|
}
|
|
break;
|
|
|
|
case 4:
|
|
buffer = this._reader.read(this._length);
|
|
if (buffer) {
|
|
this._payload = buffer;
|
|
this._emitFrame();
|
|
this._stage = 0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
_parseOpcode: function(data) {
|
|
var rsvs = [this.RSV1, this.RSV2, this.RSV3].filter(function(rsv) {
|
|
return (data & rsv) === rsv;
|
|
}, this);
|
|
|
|
if (rsvs.length > 0) return this._socket.close(this.ERRORS.protocol_error, null, false);
|
|
|
|
this._final = (data & this.FIN) === this.FIN;
|
|
this._opcode = (data & this.OPCODE);
|
|
this._mask = [];
|
|
this._payload = [];
|
|
|
|
var valid = false;
|
|
|
|
for (var key in this.OPCODES) {
|
|
if (this.OPCODES[key] === this._opcode)
|
|
valid = true;
|
|
}
|
|
if (!valid) return this._socket.close(this.ERRORS.protocol_error, null, false);
|
|
|
|
if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && !this._final)
|
|
return this._socket.close(this.ERRORS.protocol_error, null, false);
|
|
|
|
if (this._mode && this.OPENING_OPCODES.indexOf(this._opcode) >= 0)
|
|
return this._socket.close(this.ERRORS.protocol_error, null, false);
|
|
|
|
this._stage = 1;
|
|
},
|
|
|
|
_parseLength: function(data) {
|
|
this._masked = (data & this.MASK) === this.MASK;
|
|
this._length = (data & this.LENGTH);
|
|
|
|
if (this._length >= 0 && this._length <= 125) {
|
|
this._stage = this._masked ? 3 : 4;
|
|
} else {
|
|
this._lengthBuffer = [];
|
|
this._lengthSize = (this._length === 126 ? 2 : 8);
|
|
this._stage = 2;
|
|
}
|
|
},
|
|
|
|
_parseExtendedLength: function(buffer) {
|
|
this._length = this._getInteger(buffer);
|
|
this._stage = this._masked ? 3 : 4;
|
|
},
|
|
|
|
frame: function(data, type, code) {
|
|
if (this._closed) return null;
|
|
|
|
var isText = (typeof data === 'string'),
|
|
opcode = this.OPCODES[type || (isText ? 'text' : 'binary')],
|
|
buffer = isText ? new Buffer(data, 'utf8') : data,
|
|
insert = code ? 2 : 0,
|
|
length = buffer.length + insert,
|
|
header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10),
|
|
offset = header + (this._masking ? 4 : 0),
|
|
masked = this._masking ? this.MASK : 0,
|
|
frame = new Buffer(length + offset),
|
|
BYTE = this.BYTE,
|
|
mask, i;
|
|
|
|
frame[0] = this.FIN | opcode;
|
|
|
|
if (length <= 125) {
|
|
frame[1] = masked | length;
|
|
} else if (length <= 65535) {
|
|
frame[1] = masked | 126;
|
|
frame[2] = Math.floor(length / 256);
|
|
frame[3] = length & BYTE;
|
|
} else {
|
|
frame[1] = masked | 127;
|
|
frame[2] = Math.floor(length / Math.pow(2,56)) & BYTE;
|
|
frame[3] = Math.floor(length / Math.pow(2,48)) & BYTE;
|
|
frame[4] = Math.floor(length / Math.pow(2,40)) & BYTE;
|
|
frame[5] = Math.floor(length / Math.pow(2,32)) & BYTE;
|
|
frame[6] = Math.floor(length / Math.pow(2,24)) & BYTE;
|
|
frame[7] = Math.floor(length / Math.pow(2,16)) & BYTE;
|
|
frame[8] = Math.floor(length / Math.pow(2,8)) & BYTE;
|
|
frame[9] = length & BYTE;
|
|
}
|
|
|
|
if (code) {
|
|
frame[offset] = Math.floor(code / 256) & BYTE;
|
|
frame[offset+1] = code & BYTE;
|
|
}
|
|
buffer.copy(frame, offset + insert);
|
|
|
|
if (this._masking) {
|
|
mask = [Math.floor(Math.random() * 256), Math.floor(Math.random() * 256),
|
|
Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)];
|
|
new Buffer(mask).copy(frame, header);
|
|
HybiParser.mask(frame, mask, offset);
|
|
}
|
|
|
|
return frame;
|
|
},
|
|
|
|
ping: function(message, callback, context) {
|
|
message = message || '';
|
|
if (callback) this._pingCallbacks[message] = [callback, context];
|
|
return this._socket.send(message, 'ping');
|
|
},
|
|
|
|
close: function(code, reason, callback, context) {
|
|
if (this._closed) return;
|
|
if (callback) this._closingCallback = [callback, context];
|
|
this._socket.send(reason || '', 'close', code || this.ERRORS.normal_closure);
|
|
this._closed = true;
|
|
},
|
|
|
|
buffer: function(fragment) {
|
|
for (var i = 0, n = fragment.length; i < n; i++)
|
|
this._buffer.push(fragment[i]);
|
|
},
|
|
|
|
_emitFrame: function() {
|
|
var payload = HybiParser.mask(this._payload, this._mask),
|
|
opcode = this._opcode;
|
|
|
|
if (opcode === this.OPCODES.continuation) {
|
|
if (!this._mode) return this._socket.close(this.ERRORS.protocol_error, null, false);
|
|
this.buffer(payload);
|
|
if (this._final) {
|
|
var message = new Buffer(this._buffer);
|
|
if (this._mode === 'text') message = this._encode(message);
|
|
this._reset();
|
|
if (message !== null) this._socket.receive(message);
|
|
else this._socket.close(this.ERRORS.encoding_error, null, false);
|
|
}
|
|
}
|
|
else if (opcode === this.OPCODES.text) {
|
|
if (this._final) {
|
|
var message = this._encode(payload);
|
|
if (message !== null) this._socket.receive(message);
|
|
else this._socket.close(this.ERRORS.encoding_error, null, false);
|
|
} else {
|
|
this._mode = 'text';
|
|
this.buffer(payload);
|
|
}
|
|
}
|
|
else if (opcode === this.OPCODES.binary) {
|
|
if (this._final) {
|
|
this._socket.receive(payload);
|
|
} else {
|
|
this._mode = 'binary';
|
|
this.buffer(payload);
|
|
}
|
|
}
|
|
else if (opcode === this.OPCODES.close) {
|
|
var code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : null,
|
|
reason = (payload.length > 2) ? this._encode(payload.slice(2)) : null;
|
|
|
|
if (!(payload.length === 0) &&
|
|
!(code !== null && code >= 3000 && code < 5000) &&
|
|
this.ERROR_CODES.indexOf(code) < 0)
|
|
code = this.ERRORS.protocol_error;
|
|
|
|
if (payload.length > 125 || (payload.length > 2 && !reason))
|
|
code = this.ERRORS.protocol_error;
|
|
|
|
this._socket.close(code, (payload.length > 2) ? reason : null, false);
|
|
if (this._closingCallback)
|
|
this._closingCallback[0].call(this._closingCallback[1]);
|
|
}
|
|
else if (opcode === this.OPCODES.ping) {
|
|
if (payload.length > 125) return this._socket.close(this.ERRORS.protocol_error, null, false);
|
|
this._socket.send(payload, 'pong');
|
|
}
|
|
else if (opcode === this.OPCODES.pong) {
|
|
var callbacks = this._pingCallbacks,
|
|
message = this._encode(payload),
|
|
callback = callbacks[message];
|
|
|
|
delete callbacks[message];
|
|
if (callback) callback[0].call(callback[1]);
|
|
}
|
|
},
|
|
|
|
_reset: function() {
|
|
this._mode = null;
|
|
this._buffer = [];
|
|
},
|
|
|
|
_encode: function(buffer) {
|
|
try {
|
|
var string = buffer.toString('binary', 0, buffer.length);
|
|
if (!this.UTF8_MATCH.test(string)) return null;
|
|
} catch (e) {}
|
|
return buffer.toString('utf8', 0, buffer.length);
|
|
},
|
|
|
|
_getInteger: function(bytes) {
|
|
var number = 0;
|
|
for (var i = 0, n = bytes.length; i < n; i++)
|
|
number += bytes[i] << (8 * (n - 1 - i));
|
|
return number;
|
|
}
|
|
};
|
|
|
|
for (var key in instance)
|
|
HybiParser.prototype[key] = instance[key];
|
|
|
|
module.exports = HybiParser;
|
|
|