Fully end to end encrypted anonymous chat program. Server only stores public key lookup for users and the encrypted messages. No credentials are transfered to the server, but kept in local browser storage. This allows 100% safe chatting.
https://safechat.ch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
419 lines
12 KiB
419 lines
12 KiB
var MAX_PACKET_LENGTH = Math.pow(2, 24) - 1; |
|
var MUL_32BIT = Math.pow(2, 32); |
|
var PacketHeader = require('./PacketHeader'); |
|
var BigNumber = require('bignumber.js'); |
|
|
|
module.exports = Parser; |
|
function Parser(options) { |
|
options = options || {}; |
|
|
|
this._supportBigNumbers = options.config && options.config.supportBigNumbers; |
|
this._buffer = new Buffer(0); |
|
this._longPacketBuffers = []; |
|
this._offset = 0; |
|
this._packetEnd = null; |
|
this._packetHeader = null; |
|
this._packetOffset = null; |
|
this._onError = options.onError || function(err) { throw err; }; |
|
this._onPacket = options.onPacket || function() {}; |
|
this._nextPacketNumber = 0; |
|
this._encoding = 'utf-8'; |
|
this._paused = false; |
|
} |
|
|
|
Parser.prototype.write = function(buffer) { |
|
this.append(buffer); |
|
|
|
while (!this._paused) { |
|
if (!this._packetHeader) { |
|
if (this._bytesRemaining() < 4) { |
|
break; |
|
} |
|
|
|
this._packetHeader = new PacketHeader( |
|
this.parseUnsignedNumber(3), |
|
this.parseUnsignedNumber(1) |
|
); |
|
|
|
if (this._packetHeader.number !== this._nextPacketNumber) { |
|
var err = new Error( |
|
'Packets out of order. Got: ' + this._packetHeader.number + ' ' + |
|
'Expected: ' + this._nextPacketNumber |
|
); |
|
|
|
err.code = 'PROTOCOL_PACKETS_OUT_OF_ORDER'; |
|
err.fatal = true; |
|
|
|
this._onError(err); |
|
} |
|
|
|
this.incrementPacketNumber(); |
|
} |
|
|
|
if (this._bytesRemaining() < this._packetHeader.length) { |
|
break; |
|
} |
|
|
|
this._packetEnd = this._offset + this._packetHeader.length; |
|
this._packetOffset = this._offset; |
|
|
|
if (this._packetHeader.length === MAX_PACKET_LENGTH) { |
|
this._longPacketBuffers.push(this._buffer.slice(this._packetOffset, this._packetEnd)); |
|
|
|
this._advanceToNextPacket(); |
|
continue; |
|
} |
|
|
|
this._combineLongPacketBuffers(); |
|
|
|
// Try...finally to ensure exception safety. Unfortunately this is costing |
|
// us up to ~10% performance in some benchmarks. |
|
var hadException = true; |
|
try { |
|
this._onPacket(this._packetHeader); |
|
hadException = false; |
|
} catch (err) { |
|
if (!err || typeof err.code !== 'string' || err.code.substr(0, 7) !== 'PARSER_') { |
|
// Rethrow unknown errors |
|
throw err; |
|
} |
|
|
|
// Pass down parser errors |
|
this._onError(err); |
|
hadException = false; |
|
} finally { |
|
this._advanceToNextPacket(); |
|
|
|
// If we had an exception, the parser while loop will be broken out |
|
// of after the finally block. So we need to make sure to re-enter it |
|
// to continue parsing any bytes that may already have been received. |
|
if (hadException) { |
|
process.nextTick(this.write.bind(this)); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
Parser.prototype.append = function append(chunk) { |
|
if (!chunk || chunk.length === 0) { |
|
return; |
|
} |
|
|
|
var buffer = chunk; |
|
var sliceEnd = this._buffer.length; |
|
var sliceStart = this._packetOffset === null |
|
? this._offset |
|
: this._packetOffset; |
|
var sliceLength = sliceEnd - sliceStart; |
|
|
|
if (sliceLength !== 0) { |
|
// Create a new Buffer |
|
buffer = new Buffer(sliceLength + chunk.length); |
|
|
|
// Copy data |
|
this._buffer.copy(buffer, 0, sliceStart, sliceEnd); |
|
chunk.copy(buffer, sliceLength); |
|
} |
|
|
|
// Adjust data-tracking pointers |
|
this._buffer = buffer; |
|
this._offset = this._offset - sliceStart; |
|
this._packetEnd = this._packetEnd !== null |
|
? this._packetEnd - sliceStart |
|
: null; |
|
this._packetOffset = this._packetOffset !== null |
|
? this._packetOffset - sliceStart |
|
: null; |
|
}; |
|
|
|
Parser.prototype.pause = function() { |
|
this._paused = true; |
|
}; |
|
|
|
Parser.prototype.resume = function() { |
|
this._paused = false; |
|
|
|
// nextTick() to avoid entering write() multiple times within the same stack |
|
// which would cause problems as write manipulates the state of the object. |
|
process.nextTick(this.write.bind(this)); |
|
}; |
|
|
|
Parser.prototype.peak = function() { |
|
return this._buffer[this._offset]; |
|
}; |
|
|
|
Parser.prototype.parseUnsignedNumber = function(bytes) { |
|
if (bytes === 1) { |
|
return this._buffer[this._offset++]; |
|
} |
|
|
|
var buffer = this._buffer; |
|
var offset = this._offset + bytes - 1; |
|
var value = 0; |
|
|
|
if (bytes > 4) { |
|
var err = new Error('parseUnsignedNumber: Supports only up to 4 bytes'); |
|
err.offset = (this._offset - this._packetOffset - 1); |
|
err.code = 'PARSER_UNSIGNED_TOO_LONG'; |
|
throw err; |
|
} |
|
|
|
while (offset >= this._offset) { |
|
value = ((value << 8) | buffer[offset]) >>> 0; |
|
offset--; |
|
} |
|
|
|
this._offset += bytes; |
|
|
|
return value; |
|
}; |
|
|
|
Parser.prototype.parseLengthCodedString = function() { |
|
var length = this.parseLengthCodedNumber(); |
|
|
|
if (length === null) { |
|
return null; |
|
} |
|
|
|
return this.parseString(length); |
|
}; |
|
|
|
Parser.prototype.parseLengthCodedBuffer = function() { |
|
var length = this.parseLengthCodedNumber(); |
|
|
|
if (length === null) { |
|
return null; |
|
} |
|
|
|
return this.parseBuffer(length); |
|
}; |
|
|
|
Parser.prototype.parseLengthCodedNumber = function parseLengthCodedNumber() { |
|
if (this._offset >= this._buffer.length) { |
|
var err = new Error('Parser: read past end'); |
|
err.offset = (this._offset - this._packetOffset); |
|
err.code = 'PARSER_READ_PAST_END'; |
|
throw err; |
|
} |
|
|
|
var bits = this._buffer[this._offset++]; |
|
|
|
if (bits <= 250) { |
|
return bits; |
|
} |
|
|
|
switch (bits) { |
|
case 251: |
|
return null; |
|
case 252: |
|
return this.parseUnsignedNumber(2); |
|
case 253: |
|
return this.parseUnsignedNumber(3); |
|
case 254: |
|
break; |
|
default: |
|
var err = new Error('Unexpected first byte' + (bits ? ': 0x' + bits.toString(16) : '')); |
|
err.offset = (this._offset - this._packetOffset - 1); |
|
err.code = 'PARSER_BAD_LENGTH_BYTE'; |
|
throw err; |
|
} |
|
|
|
var low = this.parseUnsignedNumber(4); |
|
var high = this.parseUnsignedNumber(4); |
|
var value; |
|
|
|
if (high >>> 21) { |
|
value = (new BigNumber(low)).plus((new BigNumber(MUL_32BIT)).times(high)).toString(); |
|
|
|
if (this._supportBigNumbers) { |
|
return value; |
|
} |
|
|
|
var err = new Error( |
|
'parseLengthCodedNumber: JS precision range exceeded, ' + |
|
'number is >= 53 bit: "' + value + '"' |
|
); |
|
err.offset = (this._offset - this._packetOffset - 8); |
|
err.code = 'PARSER_JS_PRECISION_RANGE_EXCEEDED'; |
|
throw err; |
|
} |
|
|
|
value = low + (MUL_32BIT * high); |
|
|
|
return value; |
|
}; |
|
|
|
Parser.prototype.parseFiller = function(length) { |
|
return this.parseBuffer(length); |
|
}; |
|
|
|
Parser.prototype.parseNullTerminatedBuffer = function() { |
|
var end = this._nullByteOffset(); |
|
var value = this._buffer.slice(this._offset, end); |
|
this._offset = end + 1; |
|
|
|
return value; |
|
}; |
|
|
|
Parser.prototype.parseNullTerminatedString = function() { |
|
var end = this._nullByteOffset(); |
|
var value = this._buffer.toString(this._encoding, this._offset, end); |
|
this._offset = end + 1; |
|
|
|
return value; |
|
}; |
|
|
|
Parser.prototype._nullByteOffset = function() { |
|
var offset = this._offset; |
|
|
|
while (this._buffer[offset] !== 0x00) { |
|
offset++; |
|
|
|
if (offset >= this._buffer.length) { |
|
var err = new Error('Offset of null terminated string not found.'); |
|
err.offset = (this._offset - this._packetOffset); |
|
err.code = 'PARSER_MISSING_NULL_BYTE'; |
|
throw err; |
|
} |
|
} |
|
|
|
return offset; |
|
}; |
|
|
|
Parser.prototype.parsePacketTerminatedString = function() { |
|
var length = this._packetEnd - this._offset; |
|
return this.parseString(length); |
|
}; |
|
|
|
Parser.prototype.parseBuffer = function(length) { |
|
var response = new Buffer(length); |
|
this._buffer.copy(response, 0, this._offset, this._offset + length); |
|
|
|
this._offset += length; |
|
return response; |
|
}; |
|
|
|
Parser.prototype.parseString = function(length) { |
|
var offset = this._offset; |
|
var end = offset + length; |
|
var value = this._buffer.toString(this._encoding, offset, end); |
|
|
|
this._offset = end; |
|
return value; |
|
}; |
|
|
|
Parser.prototype.parseGeometryValue = function() { |
|
var buffer = this.parseLengthCodedBuffer(); |
|
var offset = 4; |
|
|
|
if (buffer === null || !buffer.length) { |
|
return null; |
|
} |
|
|
|
function parseGeometry() { |
|
var result = null; |
|
var byteOrder = buffer.readUInt8(offset); offset += 1; |
|
var wkbType = byteOrder? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4; |
|
switch(wkbType) { |
|
case 1: // WKBPoint |
|
var x = byteOrder? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8; |
|
var y = byteOrder? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8; |
|
result = {x: x, y: y}; |
|
break; |
|
case 2: // WKBLineString |
|
var numPoints = byteOrder? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4; |
|
result = []; |
|
for(var i=numPoints;i>0;i--) { |
|
var x = byteOrder? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8; |
|
var y = byteOrder? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8; |
|
result.push({x: x, y: y}); |
|
} |
|
break; |
|
case 3: // WKBPolygon |
|
var numRings = byteOrder? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4; |
|
result = []; |
|
for(var i=numRings;i>0;i--) { |
|
var numPoints = byteOrder? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4; |
|
var line = []; |
|
for(var j=numPoints;j>0;j--) { |
|
var x = byteOrder? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8; |
|
var y = byteOrder? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8; |
|
line.push({x: x, y: y}); |
|
} |
|
result.push(line); |
|
} |
|
break; |
|
case 4: // WKBMultiPoint |
|
case 5: // WKBMultiLineString |
|
case 6: // WKBMultiPolygon |
|
case 7: // WKBGeometryCollection |
|
var num = byteOrder? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4; |
|
var result = []; |
|
for(var i=num;i>0;i--) { |
|
result.push(parseGeometry()); |
|
} |
|
break; |
|
} |
|
return result; |
|
} |
|
return parseGeometry(); |
|
}; |
|
|
|
Parser.prototype.reachedPacketEnd = function() { |
|
return this._offset === this._packetEnd; |
|
}; |
|
|
|
Parser.prototype._bytesRemaining = function() { |
|
return this._buffer.length - this._offset; |
|
}; |
|
|
|
Parser.prototype.incrementPacketNumber = function() { |
|
var currentPacketNumber = this._nextPacketNumber; |
|
this._nextPacketNumber = (this._nextPacketNumber + 1) % 256; |
|
|
|
return currentPacketNumber; |
|
}; |
|
|
|
Parser.prototype.resetPacketNumber = function() { |
|
this._nextPacketNumber = 0; |
|
}; |
|
|
|
Parser.prototype.packetLength = function() { |
|
return this._longPacketBuffers.reduce(function(length, buffer) { |
|
return length + buffer.length; |
|
}, this._packetHeader.length); |
|
}; |
|
|
|
Parser.prototype._combineLongPacketBuffers = function() { |
|
if (!this._longPacketBuffers.length) { |
|
return; |
|
} |
|
|
|
var trailingPacketBytes = this._buffer.length - this._packetEnd; |
|
|
|
var length = this._longPacketBuffers.reduce(function(length, buffer) { |
|
return length + buffer.length; |
|
}, this._bytesRemaining()); |
|
|
|
var combinedBuffer = new Buffer(length); |
|
|
|
var offset = this._longPacketBuffers.reduce(function(offset, buffer) { |
|
buffer.copy(combinedBuffer, offset); |
|
return offset + buffer.length; |
|
}, 0); |
|
|
|
this._buffer.copy(combinedBuffer, offset, this._offset); |
|
|
|
this._buffer = combinedBuffer; |
|
this._longPacketBuffers = []; |
|
this._offset = 0; |
|
this._packetEnd = this._buffer.length - trailingPacketBytes; |
|
this._packetOffset = 0; |
|
}; |
|
|
|
Parser.prototype._advanceToNextPacket = function() { |
|
this._offset = this._packetEnd; |
|
this._packetHeader = null; |
|
this._packetEnd = null; |
|
this._packetOffset = null; |
|
};
|
|
|