gntp
Version:
Full, native, Growl Notification Transport Protocol (GNTP)implementation.
691 lines (644 loc) • 33.2 kB
JavaScript
/*-------------------------
Message object
*/
var constants = require('./constants.js')
, Headers = require('./headers.js')
, Header = require('./header.js')
, Crypto = require('./crypto.js')
function Message (mimeString) {
this.type = constants.MessageTypeEnum.UNKNOWN;
this.sections = [];
this.headers = new Headers();
this.crypto = null;
this.chunkSize = 1024;
this.raw = new Buffer(this.chunkSize);
this.parseInfo = {
lastPosition : 0
, received : 0
, sections : {
current : 0
, expected : 1
, lines: 0
}
, resource : {
header : undefined
, length : -1
, identifier: undefined
, section : 0
}
, error : undefined
, complete : false
};
if( typeof mimeString === 'string' ){
this.parse(mimeString);
}
}
module.exports = Message;
Message.prototype.parse = function (mime){
if ( !(mime instanceof Buffer) ){
return undefined;
}
if( mime.length + this.parseInfo.received > this.raw.length ){
var upSize = mime.length+this.parseInfo.received;
var tmp = new Buffer(upSize);
this.raw.copy(tmp);
this.raw = tmp;
}
mime.copy(this.raw,this.parseInfo.received);
this.parseInfo.received += mime.length;
var self = this,
len = this.parseInfo.received,
hdr,
resourceLength = 0,
prev = this.parseInfo.lastPosition,
curr = 0,
lines = [],
work,
line,
lineEnd,
gap,
protocol,
security,
resource = false,
parts,
block,
//expectedSections = this.parseInfo.sections.expected,
header = new Header(); // i need to fix this object...
var findEndSection = function (buff,start){
if( buff instanceof Buffer){
if( buff.length >= 4 &&
start < buff.length){
for( var i=start; i < buff.length -3; i++ ){
if( buff[i+0] == 13 && buff[i+1] == 10 &&
buff[i+2] == 13 && buff[i+3] == 10 ){
//End!
return i;
}
}
}
}
return -1;
};
var findEndLine = function (buff){
if( ( buff instanceof Buffer ) && buff.length >= 2){
for( var i=0; i < buff.length -1; i++ ){
if( buff[i+0] == 13 && buff[i+1] == 10 ){
//End!
return i;
}
}
}
return -1;
};
var processBlock = function(block){
var lines = block.toString('utf8').split('\r\n');
for( var i=0; i < lines.length; i++){
line = lines[i];
parts = [ line.substring(0,line.indexOf(':')), line.substring(line.indexOf(':')+2)];
hdr = new Header(parts[0],parts[1]);
// NotificationsCount will increase expected sections by x.
if( hdr.name === constants.HeaderEnum.notificationsCount ) {
self.parseInfo.sections.expected += Number(hdr.value);
//console.log('notificationsCount: '+ hdr.value);
}
// Resource Pointers will increase expected sections by 2
if( hdr.isGrowlResourcePointer ) {
//console.log('resourceIdentifier found:'+hdr.value);
self.parseInfo.sections.expected += 2;
}
// add it to the message headers.
self.headers.addHeader(hdr);
}
//console.log('sections (have/expecting): '+self.parseInfo.sections.current +'/'+self.parseInfo.sections.expected);
};
while(curr < len && curr > -1){
curr = findEndSection(this.raw,prev);
if( curr > -1 ){
this.parseInfo.sections.current++;
work = this.raw.slice(prev,curr);
if( this.parseInfo.sections.current == 1 ){
// always in section 1:
// GNTP protocol header
// & headers (which may be an encrypted block)
// parse protocol string, and setup crypto as required.
lineEnd = findEndLine(work,0);
if( lineEnd === -1 ){
console.log('EOL failure: '+work.length);
return undefined;
}
line = work.slice(0,lineEnd).toString('utf8');
gap = line.indexOf(' ');
protocol = line.substring(0,gap).split('/');
if( protocol[0] != constants.Protocol.name || protocol[1] != constants.Protocol.version ){
console.log('bad protocol/version');
throw 'GNTP protocol mismatch';
}
this.type = line.substring(gap+1,line.indexOf(' ',gap+1));
//console.log('message type: ' + this.type + ' => ' + (this.type.substr(0,1) === '-'?'response':'request'));
// check for the password & encryption
gap = line.indexOf(' ',gap+1);
//console.log('pwd & sec: '+gap);
security = line.substring(line.indexOf(' ',gap)+1);
//console.log('sec string: "'+security+'"');
gap = security.indexOf(' ');
//console.log('sec str gap:'+gap);
//TODO: dont hardset the password..
this.crypto = new Crypto('nodejs','sha1');
if( gap > -1 ){
security = security.split(' ');
if( security[0] !== 'NONE' ){
security[0] = security[0].split(':');
this.crypto.useEncryption = true;
this.crypto.encryptAlg = security[0][0].toLowerCase();
this.crypto.iv = new Buffer(security[0][1].length/2);
this.crypto.iv.write(security[0][1],'hex');
}
security[1] = security[1].split(':');
this.crypto.hashAlg = security[1][0].toLowerCase();
security[1][1] = security[1][1].split('.');
if( !this.crypto.setKeyString(security[1][1][0],security[1][1][1]) ){
throw 'bad authentication';
}
}else{
this.crypto.password = '';
}
block = new Buffer(work.slice(lineEnd+2).length);
work.slice(lineEnd+2).copy(block,0,0);
if( this.crypto.useEncryption ){
// decrypt the block first.
block = this.crypto.decryptBuffer(block);
}
// process the headers
processBlock(block);
}else{
// is this is the data part of a resource, handle accordingly.
if( this.parseInfo.resource.length > 0){
//console.log('reading RP: rl['+ this.parseInfo.resource.length + '] wl['+ work.length + ']');
if( this.parseInfo.resource.length === work.length ){
// find the associated header
for( var h=0; h<this.headers.pointerHeaders.length; h++){
if( this.headers.pointerHeaders[h].value === this.parseInfo.resource.identifier ){
hdr = this.headers.pointerHeaders[h]
}
}
// this goes directly to a buffer
if( this.crypto.useEncryption ){
hdr.resourceData = this.crypto.decryptBuffer(work);
}else{
hdr.resourceData = new Buffer(work.length);
work.copy(hdr.resourceData,0,0);
}
// add it to the messages
//this.headers.addHeader(hdr);
// reset resource information.
this.parseInfo.resource.length = 0;
this.parseInfo.resource.current = 0;
this.parseInfo.resource.identifier = undefined;
}else{
// we're in the middle of a resource, but haven't received it all.
console.log('partial resource');
// don't count it complete yet (decrement sections)
this.parseInfo.sections.current--;
// we should do something to bypass this break, but record it so we include it.
this.parseInfo.resource.current = curr+4;
}
}else{
// check encryption, and remember that Identifier headers are NOT encryped.
line = work.slice(0,constants.HeaderEnum.resourceIdentifier.length).toString('utf8');
//console.log('line: ['+line+'] header:'+constants.HeaderEnum.resourceIdentifier);
// check if it is a identifier
if( line.substr(0,constants.HeaderEnum.resourceIdentifier.length) === constants.HeaderEnum.resourceIdentifier){
//console.log(':: RESOURCE IDENTIFIER');
// Get ID
lineEnd = findEndLine(work,0);
line = work.toString('utf8').split(constants.EndLine);
//console.log(parts);
//console.log('I:'+line[0]);
parts = [ line[0].substring(0,line[0].indexOf(':')), line[0].substring(line[0].indexOf(':')+2)];
//console.log(parts);
hdr = new Header(parts[0],parts[1]);
this.headers.addHeader(hdr);
this.parseInfo.resource.identifier = parts[1];
// Get length
//console.log('L:'+line[1]);
parts = [ line[1].substring(0,line[1].indexOf(':')), line[1].substring(line[1].indexOf(':')+2)];
//console.log(parts);
this.parseInfo.resource.length = Number(parts[1]);
this.parseInfo.resource.current = 0;
//console.log(this.parseInfo.resource);
}else{
// Not an identifier, or identifier data
//console.log(':: NOT resource');
block = new Buffer(work.length);
work.copy(block,0,0);
if( this.crypto.useEncryption ){
// decrypt the block first.
block = this.crypto.decryptBuffer(block);
}
processBlock(block);
}
}
}
// skip past the section end marker.
prev = curr+4;
}else{
this.parseInfo.lastPosition = prev;
// No section end found.. is this the end of the message?
// If it is not, throw the rest of the message.
// What if we still expected more?
if( this.parseInfo.sections.current < this.parseInfo.sections.expected ){
// wth, why are there spares?
//console.log('EOP:false');
//console.log(this.parseInfo);
return false;
}else{
// we seem to be done...
//console.log('EOP:true');
return true;
}
}
}
// WTF making it here... This function should never reach this line.
//console.log('EOP:end!');
//console.log(this.parseInfo);
return undefined;
};
// parse_stream:
// Re-implementation of the parsing routines to work with data as a stream, not blocks.
// This should be much faster than looping block parser..
Message.prototype.parse_stream = function (mime){
// We're working on raw buffer input only
// We have to handle encryption and binary after all
if ( !(mime instanceof Buffer) ){
return undefined;
}
// expand the storage buffer if input + existing > storage
if( mime.length + this.parseInfo.received > this.raw.length ){
var upSize = mime.length+this.parseInfo.received;
var tmp = new Buffer(upSize);
this.raw.copy(tmp);
this.raw = tmp;
}
// copy the new data into storage, starting @ the previous end
mime.copy(this.raw,this.parseInfo.received);
// update out length expectations
this.parseInfo.received += mime.length;
// utility function finding EOL on the protocol
// This should probably be using the constants for the protocol EndLine
var findEndLine = function (buff, start){
if( ( buff instanceof Buffer ) && buff.length >= 2){
for( var i=start; i < buff.length -1; i++ ){
if( buff[i+0] == 13 && buff[i+1] == 10 ){
//End!
return i;
}
}
}
return -1;
};
// setup our variables
var self = this
, hdr
, currentPosition = 0
, work
, line
, lineEnd
, gap
, protocol
, security
, resource = false
, parts
, block
, header = new Header()
// processHeader
// process a header mime into header object
var processHeader = function(line){
parts = [ line.substring(0,line.indexOf(':')), line.substring(line.indexOf(':')+2)]
// handle resource length 'speshul'
if( self.parseInfo.resource.identifier ){
self.parseInfo.resource.length = Number(parts[1])
}else{
hdr = new Header(parts[0],parts[1])
// NotificationsCount will increase expected sections by x.
if( hdr.name === constants.HeaderEnum.notificationsCount ) {
self.parseInfo.sections.expected += Number(hdr.value)
}
// Resource Pointers will increase expected sections by 2
if( hdr.isGrowlResourcePointer ) {
self.parseInfo.sections.expected += 2
}
// add it to the message headers object
self.headers.addHeader(hdr);
}
}
// this.raw.length !== this.parseInfo.received
while( this.parseInfo.received - this.parseInfo.lastPosition >= constants.EndLine.length
&& currentPosition >= 0
&& !this.parseInfo.error
&& !this.parseInfo.complete){
// check for being in a resource (binary data!)
if( this.parseInfo.resource.identifier
&& this.parseInfo.resource.length > -1
&& this.parseInfo.sections.current === this.parseInfo.resource.section){
//console.log('checking resource...[len:'+this.parseInfo.resource.length+']')
// if resource.identifier & resource.length are set, we're in binary.
if( this.parseInfo.received - this.parseInfo.lastPosition >= this.parseInfo.resource.length + constants.EndLine.length ){
// we've got the whole resource.
// load the up a copy of the buffer
work = this.raw.slice(this.parseInfo.lastPosition, this.parseInfo.lastPosition+this.parseInfo.resource.length)
// decrypt if necessary
if( this.crypto.useEncryption ){
work = this.crypto.decryptBuffer(work)
}
// associate the data to the header
this.headers.pointerHeaders[this.headers.pointerHeaders.length-1].resourceData = new Buffer(work.length)
work.copy(this.headers.pointerHeaders[this.headers.pointerHeaders.length-1].resourceData,0,0)
// set the lastPosition to +resouce.length +EndLine.length
this.parseInfo.lastPosition += this.parseInfo.resource.length + constants.EndLine.length
// reset resource object
this.parseInfo.resource = {
header : undefined
, length : -1
, identifier: undefined
, section : 0
}
}else{
// we dont have it all, drop out!
currentPosition = -1;
}
}else{
// try to find an EOL
currentPosition = findEndLine(this.raw, this.parseInfo.lastPosition)
// if we've found an EOL
//console.log('currentPosition: ['+this.parseInfo.lastPosition+':'+currentPosition+'='
// + (currentPosition - this.parseInfo.lastPosition) + '] '
// + this.raw.slice(this.parseInfo.lastPosition,currentPosition).toString('utf8'))
if( currentPosition >= 0 ){
if( currentPosition == this.parseInfo.lastPosition ){
// check for a blank aka, section end.
if( this.parseInfo.sections.current == 0 && this.parseInfo.sections.lines == 0 ){
// ERROR - no protocl header, malformed.
this.parseInfo.error = constants.ErrorEnum.INVALID_REQUEST
}else{
//console.log('end section')
this.parseInfo.sections.current++
this.parseInfo.sections.lines = 0
if( this.parseInfo.sections.current === this.parseInfo.sections.expected ){
this.parseInfo.complete = true
return undefined;
}
}
}else if( this.parseInfo.sections.current == 0 && this.parseInfo.sections.lines == 0 ){
// the very first line -should- always be the protocol header.
// 0 sections & 0 lines -- beginning of message -- protocol header/encryption
line = this.raw.slice(this.parseInfo.lastPosition,currentPosition).toString('utf8')
gap = line.indexOf(' ')
part = line.substr(0,gap)
// check the protocol
//console.log( 'checking protocol: '+part.substr(0,part.indexOf('/')) )
if( part.substr(0,part.indexOf('/')) != constants.Protocol.name ){
// ERROR - unknown protocol
this.parseInfo.error = constants.ErrorEnum.UNKNOWN_PROTOCOL
}
// check the protocol version
if( !this.parseInfo.error ){
//console.log( 'checking version: '+part.substr(part.indexOf('/')+1) )
if( part.substr(part.indexOf('/')+1) != constants.Protocol.version ){
// ERROR - unknown protocol version
this.parseInfo.error = constants.ErrorEnum.UNKNOWN_PROTOCOL_VERSION
}
}
// get the message type
if( !this.parseInfo.error ){
part = line.substring(gap+1,line.indexOf(' ',gap+1))
//console.log( 'checking type: '+part )
// ensure it is known
switch (part){
case constants.MessageTypeEnum.REGISTER:
case constants.MessageTypeEnum.NOTIFY:
case constants.MessageTypeEnum.SUBSCRIBE:
case constants.MessageTypeEnum.POLL:
case constants.MessageTypeEnum.OK:
case constants.MessageTypeEnum.CALLBACK:
case constants.MessageTypeEnum.ERROR:
this.type = part
break;
default:
// ERROR - invalid directive
this.parseInfo.error = constants.ErrorEnum.INVALID_REQUEST
break;
}
}
// handle the authentication/security
if( !this.parseInfo.error ){
gap = line.indexOf(' ',gap+1);
part = line.substring(line.indexOf(' ',gap)+1)
//console.log( 'checking security: '+part)
this.crypto = new Crypto()
if( part.indexOf(' ') > -1 ){
security = part.split(' ');
// handle encryption
if( security[0] !== 'NONE' ){
// XX - do not hard code password
this.crypto.password = 'nodejs'
security[0] = security[0].split(':')
// check/handle the encryption algorithm
if( this.crypto.isValidCryptoAlgorithm(security[0][0]) ){
this.crypto.useEncryption = true
this.crypto.encryptAlg = security[0][0].toUpperCase()
this.crypto.iv = new Buffer(security[0][1].length/2)
this.crypto.iv.write(security[0][1],'hex')
}else{
// ERROR - invalid crypto algorithm
this.parseInfo.error = constants.ErrorEnum.INVALID_REQUEST
}
}
// handle the password
if( !this.parseInfo.error ){
security[1] = security[1].split(':');
this.crypto.hashAlg = security[1][0].toLowerCase();
if( this.crypto.isValidHashAlgorithm(this.crypto.hashAlg) ){
security[1][1] = security[1][1].split('.');
// XX try all valid passwords.
if( !this.crypto.setKeyString(security[1][1][0],security[1][1][1]) ){
// ERROR - not authorized
this.parseInfo.error = constants.ErrorEnum.NOT_AUTHORIZED
}
}else{
// ERROR - invalid hash algorithm
this.parseInfo.error = constants.ErrorEnum.INVALID_REQUEST
}
}
}else{
this.crypto.password = ''
}
this.parseInfo.sections.lines++
} // EOI - handle authentication/security
}else{
// 'regular' headers
// could be an encrypted block
// - never encrypted if:
// -- error :
// --- none of the message should be encrypted.
// -- resource header :
// ---- the headers are not encrypted, but the data is
// - NOTE:
// -- Should not reach this point without TYPE being defined.
if( this.type == constants.MessageTypeEnum.ERROR ){
// never encrypted - take it straight from the buffer.
processHeader(this.raw.slice(this.parseInfo.lastPosition,currentPosition).toString('utf8'))
this.parseInfo.sections.lines++
}else{
// might be encrypted, if it isn't a resource
if( this.raw.slice(this.parseInfo.lastPosition,this.parseInfo.lastPosition+constants.HeaderEnum.resourceIdentifier.length).toString('utf8')
=== constants.HeaderEnum.resourceIdentifier ){
// looks like this is the beginning of a resource header!
processHeader(this.raw.slice(this.parseInfo.lastPosition,currentPosition).toString('utf8'))
// store the identifier in the parseInfo.resource.identifier
this.parseInfo.resource.identifier = this.headers.pointerHeaders[this.headers.pointerHeaders.length -1].value
}else if( this.parseInfo.resource.identifier ){
// length is expected from the pointer
if( this.raw.slice(this.parseInfo.lastPosition,this.parseInfo.lastPosition+constants.HeaderEnum.resourceLength.length).toString('utf8')
=== constants.HeaderEnum.resourceLength ){
// this is a resource length, it is 'speshul'
processHeader(this.raw.slice(this.parseInfo.lastPosition,currentPosition).toString('utf8'))
this.parseInfo.resource.section = this.parseInfo.sections.current+1
this.parseInfo.sections.lines++
}else{
// Gah, wtf? bad behavior!
this.parseInfo.error = constants.ErrorEnum.INVALID_REQUEST
}
}else{
// not an ERROR type, and not a resource bit, check for encrypted
if( this.crypto.useEncryption ){
// grab the block & decrypt it
work = this.crypto.decryptBuffer(this.raw.slice(this.parseInfo.lastPosition,currentPosition)).toString('utf8')
// replace it in the raw and cycle back around?
// - nope, just loop on split(constants.EndLine)
work = work.split(constants.EndLine)
for( var i=0; i < work.length; i++){
processHeader(work[i])
this.parseInfo.sections.lines++
}
}else{
processHeader(this.raw.slice(this.parseInfo.lastPosition,currentPosition).toString('utf8'))
this.parseInfo.sections.lines++
}
}
}
}
this.parseInfo.lastPosition = currentPosition + constants.EndLine.length
}
} // EOI - resource check
}
return this.parseInfo.error
};
Message.prototype.protocolString = function (){
var buffer = '';
// replace NONE with the value of the crypto type.
var securityString = 'NONE';
if( this.crypto instanceof Crypto){
securityString = this.crypto.getSecurityString();
}
buffer = buffer.concat(constants.Protocol.name,'/',constants.Protocol.version,' ',this.type,' ',securityString,constants.EndLine);
return buffer;
};
Message.prototype.mime = function () {
/*
Some notes on headers:
First is all the actual text headers, then any identifiers & their data.
There is one exception here, and that is in the Register message
wherein there are the Application headers, then the Notifictions headers
for setting Notifications up, and *then* there are the identifiers.
Identifiers are formatted as such to create 2 sections per identifier:
--
Identifier: xxxxx\r\n
Length: \r\n
\r\n
data\r\n
\r\n
--
This library handles things in the register method by allowing you to
create 'blank' headers, that are strictly used by the Register method.
This needs refactored to say the least.
*/
// go through headers, section by section and type by type.
var headerBuffer = ''
, resourceBuffer=''
, h
, hBuff = ''
, rBuff = ''
, bBuff = ''
, tBuff = ''
// headers
for( h in this.headers.headers ){
if( !this.headers.headers[h].isBlank ){
headerBuffer = headerBuffer.concat(this.headers.headers[h].name,': ',this.headers.headers[h].value,constants.EndLine);
}else{
headerBuffer = headerBuffer.concat(constants.EndLine);
}
}
// customHeaders - for passing one-way information to some custom display, etc
for( h in this.headers.customHeaders ){
if( !this.headers.customHeaders[h].isBlank ){
headerBuffer = headerBuffer.concat(this.headers.customHeaders[h].name,': ',this.headers.customHeaders[h].value,constants.EndLine);
} // Blank not allowed in custom headers
}
// dataHeaders - for passing data that WILL be returned in the response
for( h in this.headers.dataHeaders ){
// erm.. yes. they just show up in the normals?
}
// this line breaks resources?
//headerBuffer = headerBuffer.concat(constants.EndLine);
// convert the utf8 text into a Buffer
hBuff = new Buffer(headerBuffer);
// If this message is supposed to be encrypted, encrypt the buffer
// - the response is a buffer object of the appropriate size, a replacement
if( this.crypto instanceof Crypto && this.crypto.useEncryption){
hBuff = this.crypto.encryptBuffer(hBuff);
bBuff = new Buffer(hBuff.length+constants.EndLine.length);
hBuff.copy(bBuff,0,0);
bBuff.write(constants.EndLine,bBuff.length-constants.EndLine.length,0);
hBuff = bBuff;
}
bBuff = new Buffer(hBuff.length+constants.EndLine.length);
hBuff.copy(bBuff,0,0);
bBuff.write(constants.EndLine,bBuff.length-2,0);
hBuff = bBuff;
// generates a section
// resourceHeaders - will generate sections
for( h in this.headers.pointerHeaders ){
if( this.headers.pointerHeaders[h].resourceData instanceof Buffer){
resourceBuffer = '';
resourceBuffer += constants.HeaderEnum.resourceIdentifier + ': ' + this.headers.pointerHeaders[h].value + constants.EndLine;
bBuff = this.headers.pointerHeaders[h].resourceData;
// If this message is supposed to be encrypted, encrypt the buffer
// - the response is a buffer object of the appropriate size, a replacement
if( this.crypto instanceof Crypto && this.crypto.useEncryption){
bBuff = this.crypto.encryptBuffer(bBuff);
//console.log('rData-enc: '+bBuff.length);
}
resourceBuffer += 'Length: ' + bBuff.length + constants.EndLine + constants.EndLine;
tBuff = new Buffer(bBuff.length+resourceBuffer.length+(constants.EndLine.length*2));
tBuff.write(resourceBuffer,0,'utf8');
bBuff.copy(tBuff,resourceBuffer.length,0,bBuff.length);
tBuff.write(constants.EndLine+constants.EndLine,resourceBuffer.length+bBuff.length,'utf8');
// we've copied out to tBuff, so bBuff can be resused
if( rBuff instanceof Buffer ) {
bBuff = rBuff;
}else{
bBuff = '';
}
rBuff = new Buffer(rBuff.length+tBuff.length);
if( bBuff instanceof Buffer) bBuff.copy(rBuff,0,0);
tBuff.copy(rBuff,bBuff.length,0);
}
}
// convert the utf8 text into a Buffer
bBuff = new Buffer(hBuff.length+rBuff.length);
hBuff.copy(bBuff,0,0);
if( rBuff instanceof Buffer ){
rBuff.copy(bBuff,hBuff.length,0);
}
return bBuff;
};