drachtio-modesl
Version:
FreeSWITCH ESL Node.js Implementation
284 lines (230 loc) • 8.62 kB
JavaScript
const { EventEmitter2 } = require('eventemitter2');
const xml2js = require('xml2js');
const Event = require('./Event');
class Parser extends EventEmitter2 {
constructor(socket) {
super({
wildcard: true,
delimiter: '::',
maxListeners: 25
});
this.bodyLen = 0;
this.encoding = 'utf8';
this.socket = socket;
this.event = null;
//buffer management: keep a list of chunks and only consolidate when needed
this._chunks = [];
this._chunksLen = 0;
this._buffer = null;
this._bufferOffset = 0;
socket.on('data', (data) => this._onData(data));
socket.on('end', () => this._onEnd());
}
//consolidate chunks into a single buffer for parsing
_consolidate() {
if (this._chunks.length === 0) {
if (!this._buffer) {
this._buffer = Buffer.alloc(0);
this._bufferOffset = 0;
}
return;
}
//merge existing unconsumed buffer portion with new chunks
const oldLen = this._buffer ? this._buffer.length - this._bufferOffset : 0;
const totalLen = oldLen + this._chunksLen;
const buf = Buffer.allocUnsafe(totalLen);
if (oldLen > 0) {
this._buffer.copy(buf, 0, this._bufferOffset);
}
let pos = oldLen;
for (let i = 0, len = this._chunks.length; i < len; ++i) {
this._chunks[i].copy(buf, pos);
pos += this._chunks[i].length;
}
this._chunks.length = 0;
this._chunksLen = 0;
this._buffer = buf;
this._bufferOffset = 0;
}
get bufferLength() {
return (this._buffer ? this._buffer.length - this._bufferOffset : 0) + this._chunksLen;
}
_onData(data) {
this._chunks.push(data);
this._chunksLen += data.length;
//if we have found a Content-Length header, parse as body
if (this.bodyLen > 0)
return this._parseBody();
//otherwise this is more headers
else
return this._parseHeaders();
}
_onEnd() {
}
_indexOfHeaderEnd() {
this._consolidate();
const buf = this._buffer;
const end = buf.length - 1;
for (let i = this._bufferOffset; i < end; ++i) {
if (buf[i] === 0x0a && buf[i + 1] === 0x0a) {
return i - this._bufferOffset;
}
}
return -1;
}
_parseHeaders() {
//get end of header marker
const headEnd = this._indexOfHeaderEnd();
//if the headers haven't ended yet, keep buffering
if (headEnd === -1) {
return;
}
//if the headers have ended pull out the header text
const headText = this._buffer.toString(this.encoding, this._bufferOffset, this._bufferOffset + headEnd);
//advance past header text + \n\n
this._bufferOffset += headEnd + 2;
//parse text into object
this.headers = this._parseHeaderText(headText);
//if there is a body to parse, attempt to parse it if we have it in the buffer
if (this.headers['Content-Length']) {
//set bodyLen so next data event with process as body
this.bodyLen = this.headers['Content-Length'];
//continue processing the buffer as body
if (this.bufferLength) this._parseBody();
}
//otherwise, this event is completed create an esl.Event object from it
else {
//an event is complete, emit it
this._parseEvent(this.headers);
//continue parsing the buffer
if (this.bufferLength) this._parseHeaders();
}
}
_parseBody() {
//haven't buffered the entire body yet, buffer some more first
if (this.bufferLength < this.bodyLen)
return;
this._consolidate();
//pull out the body
const body = this._buffer.toString(this.encoding, this._bufferOffset, this._bufferOffset + this.bodyLen);
this._bufferOffset += this.bodyLen;
this.bodyLen = 0;
//reclaim memory if we've consumed most of the buffer
if (this._bufferOffset > 65536) {
this._buffer = this._buffer.subarray(this._bufferOffset);
this._bufferOffset = 0;
}
//create the event object
this._parseEvent(this.headers, body);
//continue processing the buffer after the body of the previous event has been pulled out
this._parseHeaders();
}
_parseHeaderText(txt) {
const headers = {};
let start = 0;
for (;;) {
const nlPos = txt.indexOf('\n', start);
const line = nlPos === -1 ? txt.substring(start) : txt.substring(start, nlPos);
if (line.length === 0) {
if (nlPos === -1) break;
start = nlPos + 1;
continue;
}
const colonPos = line.indexOf(': ');
let key, value;
if (colonPos === -1) {
key = line;
value = '';
} else {
key = line.substring(0, colonPos);
value = line.substring(colonPos + 2);
}
//only call decodeURIComponent when the value contains encoded characters
if (value.indexOf('%') !== -1) {
value = decodeURIComponent(value);
}
if (key === 'Content-Length') {
headers[key] = parseInt(value, 10);
} else {
headers[key] = value;
}
if (nlPos === -1) break;
start = nlPos + 1;
}
return headers;
}
_parseXmlBody(txt) {
//in the form:
//<event>
// <headers>...</headers>
// <Content-Length>4</Content-Length> [optional]
// <body>...</body> [optional]
//</event>
const parser = new xml2js.Parser({ explicitArray: false, explicitRoot: false });
let headers = {};
//parsing the xml is synchronous, despite the callback
parser.parseString(txt, (err, data) => {
if (err) {
this.emit('error', err);
}
//do a little bit of massaging to get it into the same format
//as what a JSON message looks like
headers = data.headers;
if (data.headers['Content-Length']) {
headers['Content-Length'] = parseInt(data.headers['Content-Length'], 10);
headers._body = data.body;
}
});
return headers;
}
_parsePlainBody(txt) {
//if the body is event-plain then it is just a bunch of key/value pairs
const headerEnd = txt.indexOf('\n\n');
const headers = this._parseHeaderText(txt.substring(0, headerEnd));
if (headers['Content-Length']) {
const len = parseInt(headers['Content-Length'], 10);
const start = headerEnd + 2;
const end = start + len;
//extract body with byte length
headers._body = Buffer.from(txt).subarray(start, end).toString(this.encoding);
}
return headers;
}
_parseEvent(headers, body) {
let event, data;
switch (headers['Content-Type']) {
//parse body as JSON event data
case 'text/event-json':
try {
data = JSON.parse(body);
if (data['Content-Length'])
data['Content-Length'] = parseInt(data['Content-Length'], 10);
}
catch (e) { this.emit('error', e); }
break;
//parse body as PLAIN event data
case 'text/event-plain':
data = this._parsePlainBody(body);
break;
//parse body as XML event data
case 'text/event-xml':
data = this._parseXmlBody(body);
break;
}
if (data)
event = new Event(data);
else
event = new Event(headers, body);
//try and massage an OK/Error message
const reply = event.getHeader('Reply-Text');
if (reply) {
if (reply.indexOf('-ERR') === 0) {
event.addHeader('Modesl-Reply-ERR', reply.substring(5));
} else if (reply.indexOf('+OK') === 0) {
event.addHeader('Modesl-Reply-OK', reply.substring(4));
}
}
this.emit('esl::event', event, headers, body);
}
}
module.exports = Parser;