193 lines
4.6 KiB
JavaScript
193 lines
4.6 KiB
JavaScript
|
var fs = require('fs'),
|
||
|
protobufjs = require('protobufjs'),
|
||
|
util = require('util'),
|
||
|
Transform = require('stream').Transform;
|
||
|
|
||
|
var nameById = {
|
||
|
0: 'Version',
|
||
|
1: 'UDPTunnel',
|
||
|
2: 'Authenticate',
|
||
|
3: 'Ping',
|
||
|
4: 'Reject',
|
||
|
5: 'ServerSync',
|
||
|
6: 'ChannelRemove',
|
||
|
7: 'ChannelState',
|
||
|
8: 'UserRemove',
|
||
|
9: 'UserState',
|
||
|
10: 'BanList',
|
||
|
11: 'TextMessage',
|
||
|
12: 'PermissionDenied',
|
||
|
13: 'ACL',
|
||
|
14: 'QueryUsers',
|
||
|
15: 'CryptSetup',
|
||
|
16: 'ContextActionModify',
|
||
|
17: 'ContextAction',
|
||
|
18: 'UserList',
|
||
|
19: 'VoiceTarget',
|
||
|
20: 'PermissionQuery',
|
||
|
21: 'CodecVersion',
|
||
|
22: 'UserStats',
|
||
|
23: 'RequestBlob',
|
||
|
24: 'ServerConfig',
|
||
|
25: 'SuggestConfig'
|
||
|
};
|
||
|
var idByName = {};
|
||
|
for (var id in nameById) {
|
||
|
idByName[nameById[id]] = id;
|
||
|
}
|
||
|
|
||
|
// Explicitly reading with readFileSync to support brfs
|
||
|
var mumbleProto = fs.readFileSync(__dirname + '/Mumble.proto');
|
||
|
var messages = protobufjs.loadProto(mumbleProto).build('MumbleProto');
|
||
|
|
||
|
/**
|
||
|
* Encodes the given message.
|
||
|
*
|
||
|
* @param {string} name The name of the message.
|
||
|
* @param {object} payload The message to be encoded.
|
||
|
* @return {Buffer} The encoded message.
|
||
|
*/
|
||
|
function encode(name, payload) {
|
||
|
var encoded = new messages[name](payload || {}).toBuffer();
|
||
|
// toBuffer returns an ArrayBuffer when called in the browser
|
||
|
if (!Buffer.isBuffer(encoded)) {
|
||
|
encoded = Buffer.from(encoded);
|
||
|
}
|
||
|
return encoded;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A message object.
|
||
|
* @typedef {object} Message
|
||
|
* @property {string} name - Name of the message
|
||
|
* @property {object} [payload={}] - Payload of the message
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Decodes the given message.
|
||
|
*
|
||
|
* @param {number} id The id of the message.
|
||
|
* @param {Buffer} payload The encoded message.
|
||
|
* @return {object} The decoded message.
|
||
|
*/
|
||
|
function decode(id, payload) {
|
||
|
var name = nameById[id];
|
||
|
return new messages[name].decode(payload || {});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transform stream for encoding {@link Message Mumble messages}.
|
||
|
*
|
||
|
* @constructor
|
||
|
* @constructs Encoder
|
||
|
*/
|
||
|
function Encoder() {
|
||
|
// Allow use without new
|
||
|
if (!(this instanceof Encoder)) return new Encoder();
|
||
|
|
||
|
Transform.call(this, {
|
||
|
writableObjectMode: true
|
||
|
});
|
||
|
}
|
||
|
util.inherits(Encoder, Transform);
|
||
|
|
||
|
Encoder.prototype._transform = function(chunk, encoding, callback) {
|
||
|
if (typeof chunk.name !== 'string') {
|
||
|
return callback(new TypeError('chunk.name is not a string'));
|
||
|
}
|
||
|
chunk.payload = chunk.payload || {};
|
||
|
|
||
|
// First, encode the payload
|
||
|
var data;
|
||
|
if (chunk.name == 'UDPTunnel') {
|
||
|
// UDPTunnel message doesn't need encoding
|
||
|
data = chunk.payload;
|
||
|
} else {
|
||
|
try {
|
||
|
// Encode the message payload
|
||
|
data = encode(chunk.name, chunk.payload);
|
||
|
} catch (e) {
|
||
|
callback(e);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Then create the header
|
||
|
var header = new Buffer(6);
|
||
|
header.writeUInt16BE(idByName[chunk.name], 0);
|
||
|
header.writeUInt32BE(data.length, 2);
|
||
|
|
||
|
callback(null, Buffer.concat([header, data]));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Transform stream for decoding {@link Message Mumble messages}.
|
||
|
*
|
||
|
* @constructor
|
||
|
* @constructs Decoder
|
||
|
*/
|
||
|
function Decoder() {
|
||
|
// Allow use without new
|
||
|
if (!(this instanceof Decoder)) return new Decoder();
|
||
|
|
||
|
Transform.call(this, {
|
||
|
readableObjectMode: true
|
||
|
});
|
||
|
|
||
|
this._buffer = new Buffer(1024);
|
||
|
this._bufferSize = 0;
|
||
|
}
|
||
|
util.inherits(Decoder, Transform);
|
||
|
|
||
|
Decoder.prototype._transform = function(chunk, encoding, callback) {
|
||
|
// Add incoming chunk to internal buffer
|
||
|
if (this._buffer.length - this._bufferSize < chunk.length) {
|
||
|
// Old buffer is too small, replace with bigger one
|
||
|
var oldBuffer = this._buffer;
|
||
|
this._buffer = new Buffer(this._bufferSize + chunk.length);
|
||
|
oldBuffer.copy(this._buffer, 0, 0, this._bufferSize);
|
||
|
}
|
||
|
this._bufferSize += chunk.copy(this._buffer, this._bufferSize);
|
||
|
|
||
|
|
||
|
// Try to decode messages while we still have enough bytes
|
||
|
while (this._bufferSize >= 6) {
|
||
|
var type = this._buffer.readUInt16BE(0);
|
||
|
var size = this._buffer.readUInt32BE(2);
|
||
|
if (this._bufferSize < 6 + size) {
|
||
|
break; // Not enough bytes in internal buffer for the expected payload
|
||
|
}
|
||
|
|
||
|
var typeName = nameById[type];
|
||
|
var data = this._buffer.slice(6, 6 + size);
|
||
|
// Decode payload
|
||
|
var message;
|
||
|
if (typeName == 'UDPTunnel') {
|
||
|
// UDPTunnel payload is not encoded
|
||
|
message = new Buffer(data);
|
||
|
} else {
|
||
|
try {
|
||
|
message = decode(type, data);
|
||
|
} catch (e) {
|
||
|
return callback(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Shift remaining bytes to start of internal buffer
|
||
|
this._buffer.copy(this._buffer, 0, 6 + size, this._bufferSize);
|
||
|
this._bufferSize -= 6 + size;
|
||
|
|
||
|
this.push({
|
||
|
name: typeName,
|
||
|
payload: message
|
||
|
});
|
||
|
}
|
||
|
callback();
|
||
|
};
|
||
|
|
||
|
module.exports = {
|
||
|
Encoder: Encoder,
|
||
|
Decoder: Decoder,
|
||
|
messages: messages
|
||
|
};
|