From 80c0153b579b3bfb23c3c1c11bdd0f4af4d0e595 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sun, 10 May 2015 23:42:11 +0200 Subject: [PATCH] Monstrous amount of changes. Bad commit disciplin. --- README.md | 10 +- lib/session.js | 368 ++++++++++++++++++++++++++++++++++++------------- lib/utils.js | 11 +- package.json | 10 +- 4 files changed, 292 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 4536042..61171bd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This will setup a client that connects to localhost, port 2775 without username var larvitsmpp = require('larvitsmpp'); larvitsmpp.client(function(err, clientSession) { - clientSession.send({ + clientSession.sendSms({ 'from': '46701113311', 'to': '46709771337', 'message': 'Hello world' @@ -45,7 +45,7 @@ This will setup a client that connects to given host, port with username and pas throw err; } - console.log('SMS sent, smsId: ' + smsId); + console.log('SMS sent, smsId(s): ' + smsId.join(', ')); console.log('Return PDU object:'); console.log(retPduObj); }); @@ -101,10 +101,10 @@ Example code below: } // Incoming SMS! - serverSession.on('sms', function(sms, callback) { + serverSession.on('sms', function(sms) { - // It is important to run the callback since this is a part of the protocol - callback({ + // It is important to run the sms.resp() since this is a part of the protocol + sms.resp({ // Decimal value status code // Default is 0 == no error diff --git a/lib/session.js b/lib/session.js index fbb012f..be75734 100644 --- a/lib/session.js +++ b/lib/session.js @@ -2,8 +2,167 @@ var log = require('winston'), events = require('events'), + moment = require('moment'), utils = require('./utils'), - defs = require('./defs'); + defs = require('./defs'), + async = require('async'); + +/* + * Send a response to an sms + * This must be called from an sms object context + * + * @param str status - see list at defs.errors - defaults to 'ESME_ROK' - no error (OPTIONAL) + * @param func callback(err, [retPdu, ...]) + */ +function smsResp(status, callback) { + var sms = this, + err, + i, + tasks = []; + + if (typeof status === 'function') { + callback = status; + status = true; + } + + if (typeof callback !== 'function') { + callback = function() {}; + } + + if (sms.smsId === undefined) { + sms.smsId = ''; + } + + // Accept a generic positive status + if (status === 'true' || status === true || status === 0) { + status = 'ESME_ROK'; + } + + // Accept a generic negative status + if (status === 'false' || status === false || status === 1) { + status = 'ESME_RUNKNOWNERR'; // Set to unknown error in this case + } + + if (sms.pduObjs === undefined && sms.pduObj === undefined) { + err = new Error('No pdu objects found to base return PDU upon'); + log.warn('larvitsmpp: lib/session.js: smsResp() - ' + err.message); + callback(err); + return; + } + + if (sms.pduObjs === undefined) { + sms.pduObjs = [sms.pduObjs]; + } + + // Build async tasks to run the responses in parallel + i = 0; + while (sms.pduObjs[i] !== undefined) { + + + tasks.push(function(callback) { + sms.session.sendReturn( + sms.pduObj, + status, + {'message_id': sms.smsId}, + false, + callback + ); + }); + + i ++; + } + + + sms.session.sendReturn( + sms.pduObj, + status, + {'message_id': sms.smsId}, + false, + callback + ); + +} + +/** + * Send a dlr to an sms + * This must be called from an sms object context + * + * @param str status - see list at defs.consts.MESSAGE_STATE - defaults to 'DELIVERED' + * @param func callback(err, retPdu) + */ +function smsDlr(status, callback) { + var shortMessage, + dlrPduObj, + err, + sms = this; + + if (typeof status === 'function') { + callback = status; + status = undefined; + } + + if (status === undefined || status === true || status === 2 || status === 'true') { + status = 2; + } else if (defs.consts.MESSAGE_STATE[status] !== undefined) { + status = defs.consts.MESSAGE_STATE[status]; + } else if (defs.constsById.MESSAGE_STATE[status]) { + status = parseInt(status); + } else { + status = 5; // UNDELIVERABLE + } + + if (typeof callback !== 'function') { + callback = function() {}; + } + + if (sms.smsId === undefined) { + err = new Error('Trying to send DLR with no smsId.'); + log.warn('larvitsmpp: lib/session.js: smsDlr() - ' + err.message); + callback(err); + return; + } + + shortMessage = 'id:' + sms.smsId + ' sub:001 '; + + if (status === 2) { + shortMessage += 'dlvrd:1 '; + } else { + shortMessage += 'dlvrd:0 '; + } + + shortMessage += 'submit date:' + utils.smppDate(sms.submitTime); + shortMessage += ' done date:' + utils.smppDate(new Date()); + + if (status === 2) { + shortMessage += ' stat:DELIVRD err:0 text:xxx'; + } else { + shortMessage += ' stat:UNDELIVERABLE err:1 text:xxx'; + } + + dlrPduObj = { + 'cmdName': 'deliver_sm', + 'params': { + 'source_addr': sms.from, + 'destination_addr': sms.to, + 'esm_class': 4, + 'short_message': shortMessage + }, + 'tlvs': { + 'receipted_message_id': { + 'tagId': 0x001E, + 'tagName': 'receipted_message_id', + 'tagValue': sms.smsId + }, + 'message_state': { + 'tagId': 0x0427, + 'tagName': 'message_state', + 'tagValue': status + } + } + }; + + sms.session.send(dlrPduObj, false, callback); +} /** * Generic session function @@ -136,14 +295,14 @@ function session(sock) { log.debug('larvitsmpp: lib/session.js: session() - returnObj.send() - returnObj.on(incomingPduObj) - cmdName: ' + incPduObj.cmdName + ' seqNr: ' + incPduObj.seqNr + ' cmdStatus: ' + incPduObj.cmdStatus); // Make sure this is the actual response to the sent PDU - if (incPduObj.isResponse() && incPduObj.seqNr === pduObj.seqNr) { + if (incPduObj.isResp() && incPduObj.seqNr === pduObj.seqNr) { callback(null, incPduObj); if (closeAfterSend) { returnObj.closeSocket(); } } else { - err = new Error('larvitsmpp: lib/session.js: session() - returnObj.send() - returnObj.on(incomingPduObj) - Event triggered but incoming PDU is not a response or seqNr does not match. isResponse: ' + incPduObj.isResponse().toString() + ' incSeqNr: ' + incPduObj.seqNr + ' expected seqNr: ' + pduObj.seqNr); + err = new Error('larvitsmpp: lib/session.js: session() - returnObj.send() - returnObj.on(incomingPduObj) - Event triggered but incoming PDU is not a response or seqNr does not match. isResp: ' + incPduObj.isResp().toString() + ' incSeqNr: ' + incPduObj.seqNr + ' expected seqNr: ' + pduObj.seqNr); log.warn(err.message); callback(err); } @@ -189,6 +348,8 @@ function session(sock) { return; } + log.silly('larvitsmpp: lib/session.js: session() - returnObj.sendReturn() - Sending return PDU: ' + retPdu.toString('hex')); + returnObj.sockWrite(retPdu, closeAfterSend); if (typeof callback === 'function') { @@ -208,7 +369,8 @@ function session(sock) { * @param func callback(err, smsId, retPduObj) */ returnObj.sendLongSms = function(smsOptions, callback) { - var msgs = utils.splitMsg(smsOptions.message), + var smsIds = [], + msgs = utils.splitMsg(smsOptions.message), encoding = defs.encodings.detect(smsOptions.message); // Set encoding once for all message parts function sendPart(i) { @@ -233,8 +395,10 @@ function session(sock) { log.debug('larvitsmpp: lib/session.js: returnObj.sendLongSms() - pduObj: ' + JSON.stringify(pduObj)); returnObj.send(pduObj, function(err, retPduObj) { + smsIds.push(retPduObj.params.message_id); + if (typeof callback === 'function' && msgs[i + 1] === undefined) { - callback(err, retPduObj.params.message_id, retPduObj); + callback(err, smsIds, retPduObj); } else if (msgs[i + 1] !== undefined) { sendPart(i + 1); } @@ -281,97 +445,106 @@ function session(sock) { returnObj.send(pduObj, function(err, retPduObj) { if (typeof callback === 'function') { - callback(err, retPduObj.params.message_id, retPduObj); + callback(err, [retPduObj.params.message_id], retPduObj); } }); }; - /** - * Send a DLR - * - * @param obj sms - sms object - * @param bol status - defaults to true - * @param func callback(err) - */ - returnObj.sendDlr = function(sms, status, callback) { - var shortMessage = 'id:' + sms.smsId + ' sub:001 ', - dlrPduObj; + // Temporary storage for long sms parts + // These should be cleared if they lingre to long to avoid memory leaks + returnObj.longSmses = {}; - if (typeof status === 'function') { - callback = status; - status = true; - } + // Store long smses in the temporary storage + returnObj.longSms = function(pduObj) { + var smsGroupId = pduObj.params.short_message[3], + smsParts = pduObj.params.short_message[4], + longSmsId = pduObj.params.source_addr + '_' + pduObj.params.destination_addr + '_' + smsGroupId; - if (status === undefined) { - status = true; - } - - if (status) { - shortMessage += 'dlvrd:1 '; + if (returnObj.longSmses[longSmsId] === undefined) { + returnObj.longSmses[longSmsId] = { + 'created': new Date(), + 'partsCount': parseInt(smsParts), + 'pduObjs': [{ + 'partNr': pduObj.params.short_message[5], // We save this here to easier sort the array later on + 'pduObj': pduObj + }] + }; } else { - shortMessage += 'dlvrd:0 '; + returnObj.longSmses[longSmsId].pduObjs.push(pduObj); } + }; - shortMessage += 'submit date:' + utils.smppDate(sms.submitTime); - shortMessage += ' done date:' + utils.smppDate(new Date()); + // Walk through the long sms storage to investigate if we can send complete messages along + // or should remove old ones + returnObj.checkLongSmses = function() { + var smsGroupId, + smsGroup, + smsObj, + i, + curPduObj; - if (status) { - shortMessage += ' stat:DELIVRD err:0 text:xxx'; - } else { - shortMessage += ' stat:UNDELIVERABLE err:1 text:xxx'; - } - - dlrPduObj = { - 'cmdName': 'deliver_sm', - 'params': { - 'source_addr': sms.from, - 'destination_addr': sms.to, - 'esm_class': 4, - 'short_message': shortMessage - }, - 'tlvs': { - 'receipted_message_id': { - 'tagId': 0x001E, - 'tagName': 'receipted_message_id', - 'tagValue': sms.smsId - }, - 'message_state': { - 'tagId': 0x0427, - 'tagName': 'message_state', - 'tagValue': 2 - } - } - }; - - if ( ! status) { - dlrPduObj.tlvs.message_state.tagValue = 5; - } - - dlrPduObj.seqNr = 323; - - utils.objToPdu(dlrPduObj, function(err, pdu) { - if (err) { - throw err; + // Sort function to sort group parts + function sortLongSmsPdus(a, b) { + if (a.partNr < b.partNr) { + return - 1; } - utils.pduToObj(pdu, function(err, newObj) { - if (err) { - throw err; + if (a.partNr > b.partNr) { + return 1; + } + + return 0; + } + + // Callback function for emitted sms event + function smsReceived(smsData) { + delete returnObj.longSmses[smsGroupId]; + } + + for (smsGroupId in returnObj.longSmses) { + smsGroup = returnObj.longSmses[smsGroupId]; + + // All parts are accounted for! Emit sms event and clear from tmp storage + if (smsGroup.partsCount === smsGroup.pduObjs.length) { + log.debug('larvitsmpp: lib/session.js: session() - returnObj.checkLongSmses() - All parts accounted for in smsGroupId "' + smsGroupId + '", emitting sms event.'); + + smsObj = { + + // These are needed for references here and there in functions + 'session': returnObj, + 'pduObjs': smsGroup.pduObjs, + + 'from': smsGroup.pduObjs[0].params.source_addr, + 'to': smsGroup.pduObjs[0].params.destination_addr, + 'submitTime': new Date(), + 'message': '', + 'dlr': Boolean(smsGroup.pduObjs[0].params.registered_delivery), + 'sendResp': smsResp, + 'sendDlr': smsDlr + }; + + // Concatenate all the parts messages to one and set references to the session + + // First we need to sort the parts, since they can come in random order + smsObj.pduObjs.sort(sortLongSmsPdus); + + i = 0; + while (smsObj.pduObjs[i] !== undefined) { + curPduObj = smsObj.pduObjs[i]; + curPduObj.session = returnObj; + + smsObj.message += utils.decode(curPduObj.params.short_message, curPduObj.params.data_coding, curPduObj.params.short_message[0]); + + i ++; } - console.log('newObj:'); - console.log(newObj); - }); - }); + returnObj.emit('sms', smsObj); + } else if (moment(new Date()).diff(smsGroup.created, 'hours') > 24) { + log.info('larvitsmpp: lib/session.js: session() - returnObj.checkLongSmses() - smsGroupId "' + smsGroupId + '" is removed from returnObj.longSmses due to being older than 24 hours.'); - returnObj.send(dlrPduObj, function(err, retPduObj) { - console.log('WHATTA WHATTA'); - console.log(retPduObj); - - if (typeof callback === 'function') { - callback(err, retPduObj.params.message_id, retPduObj); + delete returnObj.longSmses[smsGroupId]; } - }); + } }; // Handle incomming commands. @@ -384,7 +557,7 @@ function session(sock) { // TLV message_state must exists if (pduObj.tlvs.message_state === undefined) { - log.info('larvitsmpp: lib/session.js: session() - returnObj.deliverSm() - TLV message_state is missing. SeqNr: ' + pduObj.seqNr); + log.info('larvitsmpp: lib/session.js: session() - returnObj.handleCmd.deliver_sm() - TLV message_state is missing. SeqNr: ' + pduObj.seqNr); returnObj.sendReturn(pduObj, 'ESME_RINVTLVSTREAM'); return; @@ -392,7 +565,7 @@ function session(sock) { // TLV message_state needs to be valid if (defs.constsById.MESSAGE_STATE[pduObj.tlvs.message_state.tagValue] === undefined) { - log.info('larvitsmpp: lib/session.js: session() - returnObj.deliverSm() - Invalid TLV message_state: "' + pduObj.tlvs.message_state.tagValue + '". SeqNr: ' + pduObj.seqNr); + log.info('larvitsmpp: lib/session.js: session() - returnObj.handleCmd.deliver_sm() - Invalid TLV message_state: "' + pduObj.tlvs.message_state.tagValue + '". SeqNr: ' + pduObj.seqNr); returnObj.sendReturn(pduObj, 'ESME_RINVTLVSTREAM'); return; @@ -400,7 +573,7 @@ function session(sock) { // TLV receipted_message_id must exist if (pduObj.tlvs.receipted_message_id === undefined) { - log.info('larvitsmpp: lib/session.js: session() - returnObj.deliverSm() - TLV receipted_message_id is missing. SeqNr: ' + pduObj.seqNr); + log.info('larvitsmpp: lib/session.js: session() - returnObj.handleCmd.deliver_sm() - TLV receipted_message_id is missing. SeqNr: ' + pduObj.seqNr); returnObj.sendReturn(pduObj, 'ESME_RINVTLVSTREAM'); return; @@ -427,25 +600,28 @@ function session(sock) { returnObj.handleCmd.submit_sm = function(pduObj) { var smsObj = {}; + // If esm_class is 0x40 it means this is just a part of a larger message + if (pduObj.params.esm_class === 0x40) { + returnObj.longSms(pduObj); + return; // Long messages should not get handled here at all, so cancel execution here + } + smsObj = { + + // These are needed for references here and there in functions + 'session': returnObj, + 'pduObj': pduObj, + 'from': pduObj.params.source_addr, 'to': pduObj.params.destination_addr, 'submitTime': new Date(), 'message': pduObj.params.short_message, - 'dlr': Boolean(pduObj.params.registered_delivery) + 'dlr': Boolean(pduObj.params.registered_delivery), + 'sendResp': smsResp, + 'sendDlr': smsDlr }; - function smsReceived(smsData) { - if (smsData === undefined) { - smsData = {}; - } - - smsObj.smsId = smsData.smsId; - - returnObj.sendReturn(pduObj, 'ESME_ROK', {'message_id': smsObj.smsId}); - } - - returnObj.emit('sms', smsObj, smsReceived); + returnObj.emit('sms', smsObj); }; // Handle incoming unbind @@ -534,7 +710,7 @@ function session(sock) { } else { log.verbose('larvitsmpp: lib/session.js: session() - returnObj.on(incomingPdu) - Incoming PDU parsed. Seqnr: ' + pduObj.seqNr + ' cmd: ' + pduObj.cmdName + ' cmdStatus: ' + pduObj.cmdStatus + ' hex: ' + pdu.toString('hex')); - if (pduObj.isResponse()) { + if (pduObj.isResp()) { // We do this so we can remove the dynamic event listeners to not have a memory leak returnObj.emit('incomingPduObj' + pduObj.seqNr, pduObj); diff --git a/lib/utils.js b/lib/utils.js index 4c36128..871f7ed 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -159,11 +159,16 @@ function writeBuffer(obj, cmdLength, callback) { * * @param buf buffer * @param str encoding 'ASCII', 'LATIN1' or 'UCS2' or hex values + * @param int offset - defaults to 0 * @return str in utf8 format */ -function decodeMsg(buffer, encoding) { +function decodeMsg(buffer, encoding, offset) { var checkEnc; + if (offset === undefined) { + offset = 0; + } + for (checkEnc in defs.consts.ENCODING) { if (parseInt(encoding) === defs.consts.ENCODING[checkEnc] || encoding === checkEnc) { encoding = checkEnc; @@ -175,7 +180,7 @@ function decodeMsg(buffer, encoding) { encoding = 'ASCII'; } - return defs.encodings[encoding].decode(buffer); + return defs.encodings[encoding].decode(buffer.slice(offset, buffer.length)); } function encodeMsg(str) { @@ -208,7 +213,7 @@ function pduToObj(pdu, stupidNullByte, callback) { } // Returns true if this PDU is a response to another PDU - retObj.isResponse = function() { + retObj.isResp = function() { return ! ! (this.cmdId & 0x80000000); }; diff --git a/package.json b/package.json index 7cdc8b8..d46d517 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "dependencies": { "winston": "latest", "utils-merge": "latest", - "iconv-lite": "latest" + "iconv-lite": "latest", + "moment": "latest", + "async": "latest" }, "description": "Simplified SMPP implementation", "devDependencies": { @@ -27,12 +29,14 @@ "url": "https://github.com/larvit/larvitsmpp", "type": "git" }, - "version": "0.0.2beta", + "version": "0.0.3", "readmeFilename": "README.md", "readme": "larvitsmpp", "bugs": { "url": "https://github.com/larvit/larvitsmpp/issues" }, "homepage": "https://github.com/larvit/larvitsmpp", - "scripts": {} + "scripts": { + "test": "mocha" + } }