diff --git a/Deliver.js b/Deliver.js new file mode 100644 index 0000000..06b8ee9 --- /dev/null +++ b/Deliver.js @@ -0,0 +1,105 @@ +'use strict'; + +var PDU = require('./pdu'), + sprintf = require('sprintf'), + util = require('util'); + +function Deliver() +{ + + Deliver.super_.apply(this, arguments); + + /** + * + * @var SCTS + */ + this._scts; + + this.setScts(); +}; + +util.inherits(Deliver, PDU); + +/** + * set scts + * @param string|null|PDU\SCTS time + * @return Deliver + */ +Deliver.prototype.setScts = function(time) +{ + var SCTS = PDU.getModule('PDU/SCTS'); + + if(time instanceof SCTS){ + this._scts = time; + } else { + this._scts = new SCTS(time || this._getDateTime()); + } + + return this; +}; + +/** + * getter for scts + * @return SCTS + */ +Deliver.prototype.getScts = function() +{ + return this._scts; +}; + +/** + * get default datetime + * @return string + */ +Deliver.prototype._getDateTime = function() +{ + // 10 days + var time = (new Date()).getTime(); + return new Date(time + (3600*24*10*1000)); +}; + +/** + * set pdu type + * @param array params + * @return Deliver + */ +Deliver.prototype.initType = function(params) +{ + var DeliverType = require('./PDU/Type/Deliver'); + this._type = new DeliverType(params || []); + return this; +}; + +/** + * Magic method for cast to string + * @return string + */ +Deliver.prototype.toString = function() +{ + var str = ''; + + str += this.getSca().toString(); + str += this.getType().toString(); + str += this.getAddress().toString(); + str += sprintf("%02X", this.getPid().getValue()); + str += this.getDcs().toString(); + str += this.getScts().toString(); + + return str; +}; + +Deliver.prototype.getStart = function() +{ + var str = ''; + + str += this.getSca().toString(); + str += this.getType().toString(); + str += this.getAddress().toString(); + str += sprintf("%02X", this.getPid().getValue()); + str += this.getDcs().toString(); + str += this.getScts().toString(); + + return str; +}; + +module.exports = Deliver; \ No newline at end of file diff --git a/PDU/DCS.js b/PDU/DCS.js new file mode 100644 index 0000000..127110a --- /dev/null +++ b/PDU/DCS.js @@ -0,0 +1,436 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function DCS() +{ + /** + * type encoding group + * @var integer + */ + this._encodeGroup = 0x00; + + /** + * specific data for encoding + * @var integer + */ + this._dataEncoding = 0x00; + + /** + * is compressed text + * @var boolean + */ + this._compressedText = true; + + /** + * Text alphabet + * @var integer + */ + this._alphabet = DCS.ALPHABET_DEFAULT; + + /** + * use message class + * @var boolean + */ + this._useMessageClass = false; + + /** + * current class message + * @var integer + */ + this._classMessage = DCS.CLASS_NONE; + + /** + * Discard Message + * @var boolean + */ + this._discardMessage = false; + + /** + * Store Message + * @var boolean + */ + this._storeMessage = false; + + /** + * Store Message UCS2 + * @var boolean + */ + this._storeMessageUCS2 = false; + + /** + * set 4-7 bits to 1 why for this, dont know + * @var boolean + */ + this._dataCodingAndMessageClass = false; + + /** + * Message indication + * @var integer + */ + this._messageIndication = false; + + /** + * set message type + * @var integer + */ + this._messageIndicationType = false; +} + +/** + * GSM 03.38 V7.0.0 (1998-07). + */ + +DCS.CLASS_NONE = 0x00; +DCS.CLASS_MOBILE_EQUIPMENT = 0x01; +DCS.CLASS_SIM_SPECIFIC_MESSAGE = 0x02; +DCS.CLASS_TERMINAL_EQUIPMENT = 0x03; + +DCS.INDICATION_TYPE_VOICEMAIL = 0x00; +DCS.INDICATION_TYPE_FAX = 0x01; +DCS.INDICATION_TYPE_EMAIL = 0x02; +DCS.INDICATION_TYPE_OTHER = 0x03; + +DCS.ALPHABET_DEFAULT = 0x00; +DCS.ALPHABET_8BIT = 0x01; +DCS.ALPHABET_UCS2 = 0x02; // 16 bit unicode +DCS.ALPHABET_RESERVED = 0x03; + + +/** + * parse pdu string + * @return DCS + */ +DCS.parse = function() +{ + var dcs = new DCS(), + buffer = new Buffer(PDU.getPduSubstr(2), 'hex'), + byte = buffer[0]; + + dcs._encodeGroup = 0x0F&(byte>>4); + dcs._dataEncoding = 0x0F&byte; + + dcs._alphabet = (3 & (dcs._dataEncoding>>2)); + dcs._classMessage = (3 & dcs._dataEncoding); + + switch(dcs._encodeGroup){ + case 0x0C: dcs._discardMessage = true; break; + case 0x0D: dcs._storeMessage = true; break; + case 0x0E: dcs._storeMessageUCS2 = true; break; + case 0x0F: + dcs._dataCodingAndMessageClass = true; + + if(dcs._dataEncoding & (1<<2)){ + dcs._alphabet = DCS.ALPHABET_8BIT; + } + + break; + + default: + + if(dcs._encodeGroup & (1<<4)){ + dcs._useMessageClass = true; + } + + if(dcs._encodeGroup & (1<<5)){ + dcs._compressedText = true; + } + } + + if(dcs._discardMessage || dcs._storeMessage || dcs._storeMessageUCS2){ + + if(dcs._dataEncoding & (1<<3)){ + dcs._messageIndication = true; + dcs._messageIndicationType = (3 & dcs._dataEncoding); + } + + } + + return dcs; +}; + +/** + * getter byte value + * @return integer + */ +DCS.prototype.getValue = function() +{ + this._encodeGroup = 0x00; + + // set data encoding, from alphabet and message class + this._dataEncoding = (this._alphabet<<2)|(this._classMessage); + + // set message class bit + if(this._useMessageClass){ + this._encodeGroup |= (1<<4); + } else { + this._encodeGroup &= ~(1<<4); + } + + // set is compressed bit + if(this._compressedText){ + this._encodeGroup |= (1<<5); + } else { + this._encodeGroup &= ~(1<<5); + } + + // change encoding format + if(this._discardMessage || this._storeMessage || this._storeMessageUCS2){ + this._dataEncoding = 0x00; + + // set indication + if(this._messageIndication){ + this._dataEncoding |= (1<<3); + + // set message indication type + this._dataEncoding |= this._messageIndicationType; + } + + } + + // Discard Message + if(this._discardMessage){ + this._encodeGroup = 0x0C; + } + + // Store Message + if(this._storeMessage){ + this._encodeGroup = 0x0D; + } + + // Store Message UCS2 + if(this._storeMessageUCS2){ + this._encodeGroup = 0x0E; + } + + // Data Coding and Message Class + if(this._dataCodingAndMessageClass){ + // set bits to 1 + this._encodeGroup = 0x0F; + + // only class message + this._dataEncoding = 0x03&this._classMessage; + + // check encoding + switch(this._alphabet){ + case DCS.ALPHABET_8BIT: + this._dataEncoding |= (1<<2); + break; + case DCS.ALPHABET_DEFAULT: + // bit is set to 0 + break; + default: + + break; + + } + } + + // return byte value + return ((0x0F&this._encodeGroup)<<4) | (0x0F&this._dataEncoding); +}; + +/** + * method for cast to string + * @return string + */ +DCS.prototype.toString = function() +{ + return sprintf("%02X", this.getValue()); +}; + +/** + * Set store message + * @return \self + */ +DCS.prototype.setStoreMessage = function() +{ + this._storeMessage = true; + return this; +}; + +/** + * Set store message UCS2 + * @return \self + */ +DCS.prototype.setStoreMessageUCS2 = function() +{ + this._storeMessageUCS2 = true; + return this; +}; + +/** + * set message indication + * @param integer $indication + * @return \self + */ +DCS.prototype.setMessageIndication = function(indication) +{ + this._messageIndication = (1 & indication); + return this; +}; + +/** + * set message indication type + * @param integer $type + * @return \self + * @throws Error + */ +DCS.prototype.setMessageIndicationType = function(type) +{ + this._messageIndicationType = 0x03&type; + + switch(this._messageIndicationType){ + case DCS.INDICATION_TYPE_VOICEMAIL: + + break; + + case DCS.INDICATION_TYPE_FAX: + + break; + + case DCS.INDICATION_TYPE_EMAIL: + + break; + + case DCS.INDICATION_TYPE_OTHER: + + break; + + default: + throw new Error("Wrong indication type"); + } + + return this; +}; + +/** + * Set discard message + * @return \self + */ +DCS.prototype.setDiscardMessage = function() +{ + this._discardMessage = true; + return this; +}; + + +/** + * set text is compressed + * @param boolean $compressed + * @return \self + */ +DCS.prototype.setTextCompressed = function(compressed) +{ + if(compressed === undefined){ + compressed = true; + } + + this._compressedText = compressed; + return this; +}; + +/** + * get text is compressed + * @return boolean + */ +DCS.prototype.getTextCompressed = function() +{ + return !!this._compressedText; +}; + +/** + * set text alphabet + * @param integer $alphabet + * @return \self + * @throws Exception + */ +DCS.prototype.setTextAlphabet = function(alphabet) +{ + this._alphabet = (0x03&alphabet); + + switch(this._alphabet){ + case DCS.ALPHABET_DEFAULT: + this.setTextCompressed(); + break; + + case DCS.ALPHABET_8BIT: + + break; + + case DCS.ALPHABET_UCS2: + + break; + + case DCS.ALPHABET_RESERVED: + + break; + + default: + throw new Error("Wrong alphabet"); + } + + return this; +}; + +/** + * getter text alphabet + * @return integer + */ +DCS.prototype.getTextAlphabet = function() +{ + return this._alphabet; +}; + +/** + * change message class + * @param integer $class + * @return \self + * @throws Exception + */ +DCS.prototype.setClass = function(cls) +{ + this.setUseMessageClass(); + this._classMessage = (0x03&cls); + + switch(this._classMessage){ + case DCS.CLASS_NONE: + this.setUseMessageClass(false); + break; + + case DCS.CLASS_MOBILE_EQUIPMENT: + + break; + + case DCS.CLASS_SIM_SPECIFIC_MESSAGE: + + break; + + case DCS.CLASS_TERMINAL_EQUIPMENT: + + break; + + default: + throw new Error("Wrong class type"); + } + + return this; +}; + +/** + * set use message class + * @return \self + * @param boolean $use + */ +DCS.prototype.setUseMessageClass = function(use) +{ + if(use === undefined){ + use = true; + } + + this._useMessageClass = use; + return this; +}; + +module.exports = DCS; \ No newline at end of file diff --git a/PDU/Data.js b/PDU/Data.js new file mode 100644 index 0000000..5fe6fa0 --- /dev/null +++ b/PDU/Data.js @@ -0,0 +1,315 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function Data(pdu) +{ + /** + * data length + * @var integer + */ + this._size; + + /** + * text message + * @var string + */ + this._data; + + /** + * parts sms + * @var array + */ + this._parts = []; + + /** + * text message is unicode + * @var boolean + */ + this._isUnicode = false; + + /** + * message object + * @var PDU + */ + this._pdu = pdu; +} + +Data.HEADER_SIZE = 7; //UDHL + UDH + +/** + * parse pdu string + * @param PDU $pdu + * @return \self + */ +Data.parse = function(pdu) +{ + var DCS = PDU.getModule('PDU/DCS'), + Part = PDU.getModule('PDU/Data/Part'); + var data = new Data(pdu); + + if(pdu.getDcs().getTextAlphabet() === DCS.ALPHABET_UCS2){ + data._isUnicode = true; + } + + var tmp = Part.parse(data); + data._data = tmp[0]; + data._size = tmp[1]; + var part = tmp[2]; + + data._parts.push(part); + + return data; +}; + +/** + * merge parts + * @param PDU $pdu + */ +Data.prototype.append = function(pdu) +{ + pdu.getParts().forEach(function(part){ + if( ! this._partExists(part)){ + this._parts.push(part); + } + }); + + this._sortParts(); +}; + +/** + * check exists new part + * @param Data\Part $part + * @throws Exception if not equals pointers + * @return boolean + */ +Data.prototype._partExists = function(part) +{ + var result = false; + this._parts.forEach(function(_part){ + if(part.getHeader().getPointer() !== _part.getHeader().getPointer()){ + throw new Error("Part from different message"); + } + + if(_part.getHeader().getCurrent() === part.getHeader().getCurrent()){ + result = false; + return false; + } + }); + + return result; +}; + +/** + * sorting parts + */ +Data.prototype._sortParts = function() +{ + this._parts.sort(function(part1, part2){ + var index1 = part1.getHeader().getCurrent(), + index2 = part2.getHeader().getCurrent(); + + return index1 > index2 ? 1 : -1; + }); + + this._data = this._parts.map(function(part){ + return part.getText(); + }).join(''); +}; + +/** + * set text message + * @param string $data + */ +Data.prototype.setData = function(data) +{ + this._data = data; + + // encode message + this._checkData(); + + // preapre parts + this._prepareParts(); +}; + +/** + * check message + */ +Data.prototype._checkData = function() +{ + var Helper = PDU.getModule('PDU/Helper'); + + // set is unicode to false + this._isUnicode = false; + // set zero size + this._size = 0; + + // check message + for(var i = 0; i < this._data.length; i++){ + // get byte + var byte = Helper.order(this._data.substr(i, 1)); + + if(byte > 0xC0){ + this._isUnicode = true; + } + + this._size++; + } + +}; + +/** + * prepare parts of message + * @throws Exception + */ +Data.prototype._prepareParts = function() +{ + var DCS = PDU.getModule('PDU/DCS'), + Helper = PDU.getModule('PDU/Helper'), + Part = PDU.getModule('PDU/Data/Part'); + var headerSize = Data.HEADER_SIZE; + var max = Helper.getLimit('normal'); + + if(this._isUnicode){ + // max length sms to unicode + max = Helper.getLimit('unicode'); + // can't compress message + this.getPdu() + .getDcs() + .setTextCompressed(false) // no compress + .setTextAlphabet(DCS.ALPHABET_UCS2); // type alphabet is UCS2 + } + + // if message is compressed + if(this.getPdu().getDcs().getTextCompressed()){ + max = Helper.getLimit('compress'); + headerSize++; + } + + var parts = this._splitMessage(max, headerSize), + header = (parts.length > 1), + uniqid = Math.floor(Math.random() * 0xFFFF); + + // message will be splited, need headers + if(header){ + this.getPdu().getType().setUdhi(1); + } + + var self = this; + parts.forEach(function(text, index){ + + PDU.debug("Part: [" + index + "] " + text); + var params = (header ? {'SEGMENTS': parts.length,'CURRENT': (index+1),'POINTER': uniqid} : undefined); + + var part = null, + size = 0, + tmp; + + switch(self.getPdu().getDcs().getTextAlphabet()){ + + case DCS.ALPHABET_DEFAULT: + PDU.debug("Helper.encode7bit(text)"); + tmp = Helper.encode7bit(text); + break; + + case DCS.ALPHABET_8BIT: + PDU.debug("Helper.encode8Bit(text)"); + tmp = Helper.encode8Bit(text); + break; + + case DCS.ALPHABET_UCS2: + PDU.debug("Helper.encode16Bit(text)"); + tmp = Helper.encode16Bit(text); + break; + + default: + throw new Eerror("Unknown alphabet"); + } + + size = tmp[0]; + part = tmp[1]; + + if(header){ + size += headerSize; + } + + self._parts.push(new Part(self, part, size, params)); + }); + +}; + +/** + * split message + * @param integer $max + * @return array + */ +Data.prototype._splitMessage = function(max, header) +{ + if(header === undefined){ + header = Data.HEADER_SIZE; + } + + // size less or equal max + if(this.getSize() <= max){ + return [this._data]; + } + + // parts of message + var data = [], + offset = 0, + size = max - header; + + while(true) + { + var part = this._data.substr(offset, size); + data.push(part); + offset += size; + + if(offset >= this.getSize()){ + break; + } + + } + + return data; +}; + + +/** + * getter text message + * @return string + */ +Data.prototype.getData = function() +{ + return this._data; +}; + +/** + * getter pdu + * @return PDU + */ +Data.prototype.getPdu = function() +{ + return this._pdu; +}; + +/** + * getter data size + * @return integer + */ +Data.prototype.getSize = function() +{ + return this._size; +}; + +/** + * get message parts + * @return array + */ +Data.prototype.getParts = function() +{ + return this._parts; +}; + +module.exports = Data; \ No newline at end of file diff --git a/PDU/Data/Header.js b/PDU/Data/Header.js new file mode 100644 index 0000000..d1a130b --- /dev/null +++ b/PDU/Data/Header.js @@ -0,0 +1,166 @@ +'use strict'; + +var PDU = require('../../pdu'), + sprintf = require('sprintf'); + +function Header(params) +{ + /** + * + * @var integer + */ + this._UDHL = 6; + + /** + * + * @var integer + */ + this._TYPE = 0x08; // 16bit + + /** + * + * @var integer + */ + this._PSIZE = 4; + + /** + * + * @var integer + */ + this._POINTER = 0; + + /** + * + * @var integer + */ + this._SEGMENTS = 1; + + /** + * + * @var integer + */ + this._CURRENT = 1; + + params = params || {}; + + this._SEGMENTS = params.SEGMENTS || 1; + this._CURRENT = params.CURRENT || 1; + this._POINTER = params.POINTER || Math.floor(Math.random() * 0xFFFF); +}; + +/** + * parse header + * @return Header + */ +Header.parse = function() +{ + var buffer = new Buffer(PDU.getPduSubstr(6), 'hex'), + udhl = buffer[0], + type = buffer[1], + psize = buffer[2]; + buffer = new Buffer(PDU.getPduSubstr((psize - 2) * 2 ), 'hex'); // psize is pointer + segments + current + var pointer = buffer.length === 1 ? buffer[0] : (buffer[0]<<8) | buffer[1]; + buffer = new Buffer(PDU.getPduSubstr(4), 'hex'); + var sergments = buffer[0], + current = buffer[1]; + + var self = new Header({ + 'UDHL': udhl, + 'TYPE': type, + 'PSIZE': psize, + 'POINTER': pointer, + 'SEGMENTS': sergments, + 'CURRENT': current + }); + + return self; +}; + +/** + * cast object to array + * @return array + */ +Header.prototype.toJSON = function() +{ + return { + 'UDHL': this._UDHL, + 'TYPE': this._TYPE, + 'PSIZE': this._PSIZE, + 'POINTER': this._POINTER, + 'SEGMENTS': this._SEGMENTS, + 'CURRENT': this._CURRENT + }; +}; + +/** + * get header size + * @return integer + */ +Header.prototype.getSize = function() +{ + return this._UDHL; +}; + +/** + * get header type + * @return integer + */ +Header.prototype.getType = function() +{ + return this._TYPE; +}; + +/** + * get a pointer size + * @return integer + */ +Header.prototype.getPointerSize = function() +{ + return this._PSIZE; +}; + +/** + * get a pointer + * @return integer + */ +Header.prototype.getPointer = function() +{ + return this._POINTER; +}; + +/** + * get a segments + * @return integer + */ +Header.prototype.getSegments = function() +{ + return this._SEGMENTS; +}; + +/** + * get current segment + * @return integer + */ +Header.prototype.getCurrent = function() +{ + return this._CURRENT; +}; + +/** + * method for cast to string + * @return string + */ +Header.prototype.toString = function() +{ + var head = ''; + head += sprintf("%02X", this._UDHL); + head += sprintf("%02X", this._TYPE); + head += sprintf("%02X", this._PSIZE); + head += sprintf("%04X", this._POINTER); + head += sprintf("%02X", this._SEGMENTS); + head += sprintf("%02X", this._CURRENT); + + return head; +}; + +module.exports = Header; \ No newline at end of file diff --git a/PDU/Data/Part.js b/PDU/Data/Part.js new file mode 100644 index 0000000..ddca007 --- /dev/null +++ b/PDU/Data/Part.js @@ -0,0 +1,177 @@ +'use strict'; + +var PDU = require('../../pdu'), + sprintf = require('sprintf'); + +function Part(parent, data, size, header) +{ + /** + * header message + * @var \Header + */ + this._header; + + /** + * data in pdu format + * @var string + */ + this._data = data; + + /** + * text message + * @var string + */ + this._text; + + /** + * size this part + * @var integer + */ + this._size = size; + + /** + * pdu data + * @var \Data + */ + this._parent = parent; + + // have params for header + if(header){ + var Header = PDU.getModule('PDU/Data/Header'); + // create header + this._header = new Header(header); + } +}; + +/** + * parse pdu string + * @param Data data + * @return array [decoded text, text size, self object] + * @throws Error + */ +Part.parse = function(data) +{ + var Header = PDU.getModule('PDU/Data/Header'), + Helper = PDU.getModule('PDU/Helper'), + DCS = PDU.getModule('PDU/DCS'); + + var alphabet = data.getPdu().getDcs().getTextAlphabet(), + header = null, + length = data.getPdu().getUdl() * (alphabet === DCS.ALPHABET_UCS2 ? 4 : 2), + text = undefined; + + if(data.getPdu().getType().getUdhi()){ + PDU.debug("Header.parse()"); + header = Header.parse(); + } + + var hex = PDU.getPduSubstr(length); + + switch(alphabet){ + case DCS.ALPHABET_DEFAULT: + text = Helper.decode7bit(hex); + break; + + case DCS.ALPHABET_8BIT: + text = Helper.decode8bit(hex); + break; + + case DCS.ALPHABET_UCS2: + text = Helper.decode16Bit(hex); + break; + + default: + throw new Error("Unknown alpabet"); + } + + var size = text.length, + self = new Part(data, hex, size, header); + + self._text = text; + + return [text, size, self]; +}; + +/** + * getter for text message + * @return string + */ +Part.prototype.getText = function() +{ + return this._text; +}; + +/** + * getter data + * @return string + */ +Part.prototype.getData = function() +{ + return this._data; +}; + +/** + * getter header + * @return Header + */ +Part.prototype.getHeader = function() +{ + return this._header; +}; + +/** + * getter parent of part + * @return \Data + */ +Part.prototype.getParent = function() +{ + return this._parent; +}; + +/** + * getter size + * @return integer + */ +Part.prototype.getSize = function() +{ + return this._size; +}; + +/** + * convert pdu to srting + * @return string + */ +Part.prototype._getPduString = function() +{ + return this._parent.getPdu().getStart().toString(); +}; + +/** + * to hex + * @return string + */ +Part.prototype._getPartSize = function() +{ + return sprintf("%02X", this._size); +}; + +/** + * magic method for cast part to string + * @return string + */ +Part.prototype.toString = function() +{ + PDU.debug("_getPduString() " + this._getPduString()); + PDU.debug("_getPartSize() " + this._getPartSize()); + PDU.debug("getHeader() " + this.getHeader()); + PDU.debug("getData() " + this.getData()); + + // concate pdu, size of part, headers, data + return '' + + (this._getPduString() || '') + + (this._getPartSize() || '') + + (this.getHeader() || '') + + (this.getData() || ''); +}; + +module.exports = Part; \ No newline at end of file diff --git a/PDU/Helper.js b/PDU/Helper.js new file mode 100644 index 0000000..e25f677 --- /dev/null +++ b/PDU/Helper.js @@ -0,0 +1,291 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function Helper() +{ + +} + +Helper._limitNormal = 140; +Helper._limitCompress = 160; +Helper._limitUnicode = 70; + +Helper.ucfirst = function(str) +{ + return str.substr(0, 1).toUpperCase() + str.substr(1); +}; + +/** + * set limit + * @param integer $limit + * @param string $type + */ +Helper.setLimit = function(limit, type) +{ + Helper['_limit' + Helper.ucfirst(type)] = limit; +}; + +/** + * getter for limit + * @param string $type + * @return integer + */ +Helper.getLimit = function(type) +{ + return Helper['_limit' + Helper.ucfirst(type)]; +}; + +/** + * ord() for unicode + * @param string $char + * @return integer + */ +Helper.order = function(char) +{ + return char.charCodeAt(0); +}; + +/** + * chr() for unicode + * @param integer $order + * @return string + */ +Helper.char = function(order) +{ + return String.fromCharCode(order); +}; + +/** + * decode message from unicode + * @param string $text + * @return srting + */ +Helper.decode16Bit = function(text) +{ + return text.match(/.{1,4}/g).map(function(hex){ + var buffer = new Buffer(hex, 'hex'); + return Helper.char((buffer[0]<<8) | buffer[1]); + }).join(""); +}; + +/** + * decode message + * @param string $text + * @return string + */ +Helper.decode8Bit = function(text) +{ + return text.match(/.{1,2}/g).map(function(hex){ + var buffer = new Buffer(hex, 'hex'); + return Helper.char(buffer[1]); + }).join(""); +}; + +/** + * decode message from 7bit + * @param string $text + * @return string + */ +Helper.decode7bit = function(text) +{ + var ret = [], + data = new Buffer(text, "hex"), + mask = 0xFF, + shift = 0, + carry = 0; + + for(var i = 0; i < data.length; i++){ + var char = data[i]; + if(shift === 7){ + ret.push(carry); + carry = 0; + shift = 0; + } + + var a = (mask >> (shift+1)) & 0xFF, + b = a ^ 0xFF; + + var digit = (carry) | ((char & a) << (shift)) & 0xFF; + carry = (char & b) >> (7-shift); + ret.push(digit); + + shift++; + } + + if (carry){ + ret.push(carry); + } + + return (new Buffer(ret, "binary")).toString(); +}; + +/** + * encode message + * @param string $text + * @return array + */ +Helper.encode8Bit = function(text) +{ + var length = 0, + pdu = '', + buffer = new Buffer(text, "ascii"); + + for(var i = 0; i < buffer.length; i++){ + pdu += sprintf("%02X", buffer[i]); + length++; + } + + return [length, pdu]; +}; + +/** + * encode message + * @param string $text + * @return array + */ +Helper.encode7bit = function(text) +{ + var ret = [], + data = new Buffer(text), + mask = 0xFF, + shift = 0, + len = data.length; + + for (var i = 0; i < len; i++) { + + var char = data[i] & 0x7F, + nextChar = (i+1 < len) ? (data[i+1] & 0x7F) : 0; + + if (shift === 7) { shift = 0; continue; } + + var carry = (nextChar & (((mask << (shift+1)) ^ 0xFF) & 0xFF)), + digit = ((carry << (7-shift)) | (char >> shift) ) & 0xFF; + + ret.push(digit); + + shift++; + } + + ret.unshift( + ret.map(function(){ + return "%02X"; + }).join("") + ); + + return [len, sprintf.apply(sprintf, ret)]; +}; + +/** + * encode message + * @param string $text + * @return array + */ +Helper.encode16Bit = function(text) +{ + var length = 0, + pdu = ''; + + for(var i = 0; i < text.length; i++){ + var byte = Helper.order(text.substr(i, 1)); + pdu += sprintf("%04X", byte); + length += 2; + } + + return [length, pdu]; +}; + +/** + * get pdu object by type + * @return Deliver|Submit|Report + * @throws Exception + */ +Helper.getPduByType = function() +{ + var Type = PDU.getModule('PDU/Type'); + + // parse type of sms + var type = Type.parse(), + self = null; + + switch(type.getMti()){ + case Type.SMS_DELIVER: + self = PDU.Deliver(); + break; + + case Type.SMS_SUBMIT: + self = PDU.Submit(); + + var buffer = new Buffer(PDU.getPduSubstr(2), 'hex'); + // get mr + self.setMr(buffer[0]); + break; + + case Type.SMS_REPORT: + self = PDU.Report(); + + var buffer = new Buffer(PDU.getPduSubstr(2), 'hex'); + // get reference + self.setReference(buffer[0]); + break; + + default: + throw new Error("Unknown sms type"); + + } + + // set type + self.setType(type); + + return self; +}; + +Helper.initVars = function(pdu) +{ + + var SCTS = PDU.getModule('PDU/SCTS'), + PID = PDU.getModule('PDU/PID'), + DCS = PDU.getModule('PDU/DCS'), + VP = PDU.getModule('PDU/VP'), + Data = PDU.getModule('PDU/Data'); + + // if is the report status + if(pdu.getType() instanceof require('./Type/Report')){ + // parse timestamp + pdu.setDateTime(SCTS.parse()); + + // parse discharge + pdu.setDischarge(SCTS.parse()); + + var buffer = new Buffer(PDU.getPduSubstr(2), 'hex'); + // get status + pdu.setStatus(buffer[0]); + } else { + // get pid + pdu.setPid(PID.parse()); + + // parse dcs + pdu.setDcs(DCS.parse()); + + // if this submit sms + if(pdu.getType() instanceof require('./Type/Submit')){ + // parse vp + pdu.setVp(VP.parse(pdu)); + } else { + // parse scts + pdu.setScts(SCTS.parse()); + } + + var buffer = new Buffer(PDU.getPduSubstr(2), 'hex'); + // get data length + pdu.setUdl(buffer[0]); + + // parse data + pdu.setData(Data.parse(pdu)); + } + + return pdu; +}; + +module.exports = Helper; \ No newline at end of file diff --git a/PDU/PID.js b/PDU/PID.js new file mode 100644 index 0000000..5eba336 --- /dev/null +++ b/PDU/PID.js @@ -0,0 +1,129 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function PID() +{ + /** + * pid value + * @var integer + */ + this._pid = PID.PID_ASSIGNED; + + /** + * value = 0 : no interworking, but SME-to-SME protocol + * value = 1 : telematic interworking + * @var integer + */ + this._indicates = 0x00; + + /** + * type value + * @var integer + */ + this._type = PID.TYPE_IMPLICIT; + + +} + +PID.PID_ASSIGNED = 0x00; // Assigns bits 0..5 as defined below +PID.PID_GSM_03_40 = 0x01; // See GSM 03.40 TP-PID complete definition +PID.PID_RESERVED = 0x02; // Reserved +PID.PID_SPECIFIC = 0x03; // Assigns bits 0-5 for SC specific use + +PID.TYPE_IMPLICIT = 0x00; // Implicit +PID.TYPE_TELEX = 0x01; // telex (or teletex reduced to telex format) +PID.TYPE_TELEFAX = 0x02; // group 3 telefax +PID.TYPE_VOICE = 0x04; // voice telephone (i.e. conversion to speech) +PID.TYPE_ERMES = 0x05; // ERMES (European Radio Messaging System) +PID.TYPE_NPS = 0x06; // National Paging system (known to the SC +PID.TYPE_X_400 = 0x11; // any public X.400-based message handling system +PID.TYPE_IEM = 0x12; // Internet Electronic Mail + +PID.parse = function() +{ + var buffer = Buffer(PDU.getPduSubstr(2), 'hex'), + byte = buffer[0], + self = new PID(); + + self.setPid(byte >> 6); + self.setIndicates(byte >> 5); + self.setType(byte); + + return self; +}; + +/** + * getter for the pid + * @return integer + */ +PID.prototype.getPid = function() +{ + return this._pid; +}; + +/** + * setter for the pid + * @param integer $pid + */ +PID.prototype.setPid = function(pid) +{ + this._pid = 0x03 & pid; +}; + +/** + * getter for the indicates + * @return integer + */ +PID.prototype.getIndicates = function() +{ + return this._indicates; +}; + +/** + * setter for the indicates + * @param integer $indicates + */ +PID.prototype.setIndicates = function(indicates) +{ + this._indicates = 0x01 & indicates; +}; + +/** + * getter for the type + * @return integer + */ +PID.prototype.getType = function() +{ + return this._type; +}; + +/** + * setter for the type + * @param integer $type + */ +PID.prototype.setType = function(type) +{ + this._type = 0x1F & type; +}; + +/** + * getter for ready value + * @return integer + */ +PID.prototype.getValue = function() +{ + return (this._pid << 6) | (this._indicates << 5) | this._type; +}; + +/** + * cast to string + * @return string + */ +PID.prototype.toString = function() +{ + return '' + this.getValue(); +}; + +module.exports = PID; \ No newline at end of file diff --git a/PDU/SCA.js b/PDU/SCA.js new file mode 100644 index 0000000..ddc74e3 --- /dev/null +++ b/PDU/SCA.js @@ -0,0 +1,261 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function SCA(isAddress) +{ + /** + * Type of number + * @var Type + */ + this._type = null; + + /** + * Phone size + * @var integer + */ + this._size = 0x00; + + + /** + * phone number + * @var string + */ + this._phone = null; + + /** + * recipient encoded + * @var type + */ + this._encoded = null; + + + /** + * how claclulate size (octets or digits) + * OA and DA size is on digits + * @var boolean + */ + this._isAddress = false; + + var Type = PDU.getModule('PDU/SCA/Type'); + + // create sca type + this.setType(new Type()); + + this._isAddress = !!isAddress; +} + +SCA.parse = function(isAddress) +{ + var Type = PDU.getModule('PDU/SCA/Type'), + Helper = PDU.getModule('PDU/Helper'); + + if(isAddress === undefined) isAddress = true; + + var buffer = new Buffer(PDU.getPduSubstr(2), 'hex'); + var sca = new SCA(isAddress), + size = buffer[0]; + + if(size){ + + // if is OA or DA size in digits + if(isAddress){ + if((size % 2) !== 0){ + size++; + } + // else size in octets + } else { + size--; + size *= 2; + } + + buffer = new Buffer(PDU.getPduSubstr(2), 'hex'); + sca.setType( + new Type(buffer[0]) + ); + + var hex = PDU.getPduSubstr(size); + + switch(sca.getType().getType()){ + case Type.TYPE_UNKNOWN: + case Type.TYPE_INTERNATIONAL: + case Type.TYPE_ACCEPTER_INTO_NET: + case Type.TYPE_SUBSCRIBER_NET: + case Type.TYPE_TRIMMED: + + sca.setPhone( + hex.match(/.{1,2}/g).map(function(b){ + return SCA._map_filter_decode(b) + .split("").reverse().join(""); + }).join("") + ); + + break; + + case Type.TYPE_ALPHANUMERICAL: + + sca.setPhone(Helper.decode7bit(hex)); + + break; + + } + + } + + return sca; +}; + +/** + * getter for phone + * @return string|null + */ +SCA.prototype.getPhone = function() +{ + return this._phone; +}; + +/** + * set phone number + * @param string phone + * @param boolean SC + */ +SCA.prototype.setPhone = function(phone, SC) +{ + var Helper = PDU.getModule('PDU/Helper'), + Type = PDU.getModule('PDU/SCA/Type'); + + this._phone = phone; + var clear = phone.replace(/[^a-c0-9\*\#]/gi, ''); + this._isAddress = !SC; + + if(this.getType().getType() === Type.TYPE_ALPHANUMERICAL){ + var tmp = Helper.encode7bit(clear); + this._size = tmp.shift(); + this._encoded = tmp.shift(); + } else { + + // get size + // service center addres counting by octets OA or DA as length numbers + this._size = SC ? 1 + ((clear.length + 1)/2) : clear.length; + + this._encoded = clear.split("").map(function(s){ + return SCA._map_filter_encode(s); + }).join(""); + + } + +}; + +/** + * getter for phone size + * @return integer + */ +SCA.prototype.getSize = function() +{ + return this._size; +}; + +/** + * getter for phone type + * @return Type + */ +SCA.prototype.getType = function() +{ + return this._type; +}; + +/** + * setter type + * @param Type type + */ +SCA.prototype.setType = function(type) +{ + this._type = type; +}; + +/** + * check is address + * @return boolean + */ +SCA.prototype.isAddress = function() +{ + return !!this._isAddress; +}; + +/** + * magic method for cast to string + * @return srting|null + */ +SCA.prototype.toString = function() +{ + var Type = PDU.getModule('PDU/SCA/Type'); + var str = sprintf("%02X", this.getSize()); + + if(this.getSize()){ + + str += this.getType().toString(); + + if(this.getType().getType() !== Type.TYPE_ALPHANUMERICAL){ + // reverse octets + var l = this._encoded.length; + for(var i = 0; i < l; i += 2){ + var b1 = this._encoded.substr(i, 1), + b2 = ((i + 1) >= l) ? 'F' : this._encoded.substr(i+1, 1); + + // add to pdu + str += b2 + b1; + } + } + } + + return str; +}; + +/** + * get offset + * @return integer + */ +SCA.prototype.getOffset = function() +{ + return ( ! this._size ? 2 : this._size + 4); +}; + +/** + * decode phone number + * @param string $letter + * @return string + */ +SCA._map_filter_decode = function(letter) +{ + var buffer = new Buffer(letter, 'hex'); + switch(buffer[0]){ + case 0x0A: return "*"; + case 0x0B: return "#"; + case 0x0C: return "a"; + case 0x0D: return "b"; + case 0x0E: return "c"; + default: return letter; + } +}; + + +/** + * encode phone number + * @param string $letter + * @return string + */ +SCA._map_filter_encode = function(letter) +{ + switch(letter){ + case "*": return 'A'; + case "#": return 'B'; + case "a": return 'C'; + case "b": return 'D'; + case "c": return 'E'; + default: return letter; + } +}; + + +module.exports = SCA; \ No newline at end of file diff --git a/PDU/SCA/Type.js b/PDU/SCA/Type.js new file mode 100644 index 0000000..da73976 --- /dev/null +++ b/PDU/SCA/Type.js @@ -0,0 +1,95 @@ +'use strict'; + +var PDU = require('../../pdu'), + sprintf = require('sprintf'); + +function Type(value) +{ + value = value || 0x91; + + /** + * Type of number + * @var integer + */ + this._type = 0x07 & (value>>4); + + /** + * Numbering plan identification + * @var integer + */ + this._plan = 0x0F & value; +} + +Type.TYPE_UNKNOWN = 0x00; +Type.TYPE_INTERNATIONAL = 0x01; +Type.TYPE_NATIONAL = 0x02; +Type.TYPE_ACCEPTER_INTO_NET = 0x03; +Type.TYPE_SUBSCRIBER_NET = 0x04; +Type.TYPE_ALPHANUMERICAL = 0x05; +Type.TYPE_TRIMMED = 0x06; +Type.TYPE_RESERVED = 0x07; + +Type.PLAN_UNKNOWN = 0x00; +Type.PLAN_ISDN = 0x01; +Type.PLAN_X_121 = 0x02; +Type.PLAN_TELEX = 0x03; +Type.PLAN_NATIONAL = 0x08; +Type.PLAN_INDIVIDUAL = 0x09; +Type.PLAN_ERMES = 0x0A; +Type.PLAN_RESERVED = 0x0F; + +/** + * setter for type of number + * @param type $type + */ +Type.prototype.setType = function(type) +{ + this._type = 0x07 & type; +}; + +/** + * getter for type of number + * @return integer + */ +Type.prototype.getType = function() +{ + return this._type; +}; + +/** + * setter for numbering plan identification + * @param type $plan + */ +Type.prototype.setPlan = function(plan) +{ + this._plan = 0x0F & plan; +}; + +/** + * getter for numbering plan identification + * @return integer + */ +Type.prototype.getPlan = function() +{ + return this._plan; +}; + +/** + * get current value + * @return integer + */ +Type.prototype.getValue = function() +{ + return (1 << 7) | (this.getType() << 4) | this.getPlan(); +}; + +/** + * magic method cast to string + * @return string + */ +Type.prototype.toString = function() +{ + return sprintf("%02X", this.getValue()); +}; + +module.exports = Type; \ No newline at end of file diff --git a/PDU/SCTS.js b/PDU/SCTS.js new file mode 100644 index 0000000..ef8ba89 --- /dev/null +++ b/PDU/SCTS.js @@ -0,0 +1,82 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function SCTS(date) +{ + /** + * unix time + * @var integer + */ + this._time = date.getTime() / 1000; +} + +/** + * parse pdu string + * @return SCTS + */ +SCTS.parse = function() +{ + var hex = PDU.getPduSubstr(14), + params = ["20%02d-%02d-%02d %02d:%02d:%02d"]; + + hex.match(/.{1,2}/g).map(function(s){ + params.push( + parseInt( + s.split("").reverse().join("") + ) + ); + }); + + var time = Date.parse(sprintf.apply(sprintf, params)), + date = new Date(time); + + return new SCTS(date); +}; + +/** + * getter time + * @return integer + */ +SCTS.prototype.getTime = function() +{ + return this._time; +}; + +/** + * format datatime for split + * @return string + */ +SCTS.prototype._getDateTime = function() +{ + var dt = new Date(this.getTime() * 1000); + return printf( + '%02d%02d%02d%02d%02d%02d00', + dt.getYear(), + dt.getMonth() + 1, + dt.getDate(), + dt.getHours(), + dt.getMinutes(), + dt.getSeconds() + ); +}; + +/** + * cast to string + * @return string + */ +SCTS.prototype.toString = function() +{ + + return this._getDateTime() + .match(/.{1,2}/g) + .map(function(s){ + return parseInt( + s.split("").reverse().join("") + ); + }).join(""); +}; + + +module.exports = SCTS; \ No newline at end of file diff --git a/PDU/Type.js b/PDU/Type.js new file mode 100644 index 0000000..12425a8 --- /dev/null +++ b/PDU/Type.js @@ -0,0 +1,191 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function Type() +{ + + + /** + * Reply Path + * @var integer + */ + this._rp; + + + /** + * User Data Header + * @var integer + */ + this._udhi; + + /** + * Status Report Request + * @var integer + */ + this._srr; + + /** + * Validity Period Format + * @var integer + */ + this._vpf; + + /** + * Reject Duplicates + * @var integer + */ + this._rd; + + /** + * Message Type Indicator + * @var integer + */ + this._mti; +}; + +Type.SMS_SUBMIT = 0x01; +Type.SMS_DELIVER = 0x00; +Type.SMS_REPORT = 0x02; + +Type.VPF_NONE = 0x00; +Type.VPF_SIEMENS = 0x01; +Type.VPF_RELATIVE = 0x02; +Type.VPF_ABSOLUTE = 0x03; + + +/** + * parse sms type + * @return Type + * @throws Error + */ +Type.parse = function() +{ + var buffer = new Buffer(PDU.getPduSubstr(2), 'hex'), + byte = buffer[0], + type = null; + + switch((3&byte)){ + case Type.SMS_DELIVER: + type = new (require('./Type/Deliver'))(); + break; + case Type.SMS_SUBMIT: + type = new (require('./Type/Submit'))(); + break; + case Type.SMS_REPORT: + type = new (require('./Type/Report'))(); + break; + default: + throw new Error("Unknown type sms"); + } + + type._rp = (1&byte>>7); + type._udhi = (1&byte>>6); + type._srr = (1&byte>>5); + type._vpf = (3&byte>>3); + type._rd = (1&byte>>2); + type._mti = (3&byte); + + return type; + +}; + +/** + * Calculate byte value + * @return integer + */ +Type.prototype.getValue = function() +{ + return ((1 & this._rp) << 7) | + ((1 & this._udhi) << 6) | + ((1 & this._srr) << 5) | + ((3 & this._vpf) << 3) | + ((1 & this._rd) << 2) | + ((3 & this._mti)); +}; + +/** + * set validity period format + * @param integer vpf + * @throws Error + */ +Type.prototype.setVpf = function(vpf) +{ + this._vpf = (0x03&vpf); + + switch(this._vpf){ + case Type.VPF_NONE: break; + case Type.VPF_SIEMENS: break; + case Type.VPF_RELATIVE: break; + case Type.VPF_ABSOLUTE: break; + default: + throw new Error("Wrong validity period format"); + } +}; + +/** + * getter for vpf + * @return integer + */ +Type.prototype.getVpf = function() +{ + return this._vpf; +}; + +/** + * set user data header + * @param type udhi + */ +Type.prototype.setUdhi = function(udhi) +{ + this._udhi = (0x01&udhi); +}; + +/** + * getter for udhi + * @return integer + */ +Type.prototype.getUdhi = function() +{ + return this._udhi; +}; + +/** + * set status report request + * @param integer srr + */ +Type.prototype.setSrr = function(srr) +{ + this._srr = (0x01&srr); +}; + +/** + * getter for status report request + * @return integer + */ +Type.prototype.getSrr = function() +{ + return this._srr; +}; + +/** + * getter for mti + * @return integer + */ +Type.prototype.getMti = function() +{ + return this._mti; +}; + +/** + * Magic method for cast to string + * @return string + */ +Type.prototype.toString = function() +{ + return sprintf("%02X", this.getValue()); +}; + + +module.exports = Type; \ No newline at end of file diff --git a/PDU/Type/Deliver.js b/PDU/Type/Deliver.js new file mode 100644 index 0000000..2aef37f --- /dev/null +++ b/PDU/Type/Deliver.js @@ -0,0 +1,27 @@ +'use strict'; + +var PDU = require('../../pdu'), + Type = require('../Type'), + sprintf = require('sprintf'), + util = require('util'); + +function Deliver(params) +{ + Deliver.super_.apply(this, arguments); + + params = params || {}; + + this._rp = params.rp ? 1 & params.rp : 0; + this._udhi = params.udhi ? 1 & params.udhi : 0; + this._srr = params.srr ? 1 & params.srr : 0; + + //More Message to Send + this._rd = params.mms ? 1 & params.mms : 0; + this._mti = 0x00; // SMS-DELIVER + this._vpf = 0x00; // not used +} + + +util.inherits(Deliver, Type); + +module.exports = Deliver; \ No newline at end of file diff --git a/PDU/Type/Report.js b/PDU/Type/Report.js new file mode 100644 index 0000000..8d5c9c7 --- /dev/null +++ b/PDU/Type/Report.js @@ -0,0 +1,27 @@ +'use strict'; + +var PDU = require('../../pdu'), + Type = require('../Type'), + sprintf = require('sprintf'), + util = require('util'); + +function Report(params) +{ + Report.super_.apply(this, arguments); + + params = params || {}; + + this._rp = params.rp ? 1 & params.rp : 0; + this._udhi = params.udhi ? 1 & params.udhi : 0; + this._srr = params.srr ? 1 & params.srr : 0; + + //More Message to Send + this._rd = params.mms ? 1 & params.mms : 0; + this._mti = 0x02; // SMS-REPORT + this._vpf = 0x00; // not used +} + + +util.inherits(Report, Type); + +module.exports = Report; \ No newline at end of file diff --git a/PDU/Type/Submit.js b/PDU/Type/Submit.js new file mode 100644 index 0000000..7077697 --- /dev/null +++ b/PDU/Type/Submit.js @@ -0,0 +1,25 @@ +'use strict'; + +var PDU = require('../../pdu'), + Type = require('../Type'), + sprintf = require('sprintf'), + util = require('util'); + +function Submit(params) +{ + Submit.super_.apply(this, arguments); + + params = params || {}; + + this._rp = params.rp ? 1 & params.rp : 0; + this._udhi = params.udhi ? 1 & params.udhi : 0; + this._srr = params.srr ? 1 & params.srr : 0; + this._vpf = params.vpf ? 3 & params.vpf : 0; + this._rd = params.rd ? 1 & params.rd : 0; + this._mti = 0x01; // SMS-SUBMIT +} + + +util.inherits(Submit, Type); + +module.exports = Submit; \ No newline at end of file diff --git a/PDU/VP.js b/PDU/VP.js new file mode 100644 index 0000000..c235621 --- /dev/null +++ b/PDU/VP.js @@ -0,0 +1,139 @@ +'use strict'; + +var PDU = require('../pdu'), + sprintf = require('sprintf'); + +function VP(submit) +{ + /** + * date time validity period + * @var string|null + */ + this._datetime; + + /** + * inteval validity period + * @var integer|null + */ + this._interval; + + /** + * pdu message + * @var Submit + */ + this._pdu = submit; +}; + + +/** + * parse pdu string + * @param PDU submit + * @return VP + * @throws Error + */ +VP.parse = function(submit) +{ + var SCTS = PDU.getModule('PDU/SCTS'), + Type = PDU.getModule('PDU/Type'); + + var vp = new VP(submit); + + switch(submit.getType().getVpf()){ + case Type.VPF_NONE: return vp; + case Type.VPF_ABSOLUTE: return SCTS.parse(); + + case Type.VPF_RELATIVE: + + var buffer = new Buffer(PDU.getPduSubstr(2), 'hex'), + byte = buffer[0]; + + if(byte <= 143){ + vp._interval = (byte+1) * (5*60); + } else if(byte <= 167){ + vp._interval = (3600*24*12) + (byte-143) * (30*60); + } else if(byte <= 196) { + vp._interval = (byte-166) * (3600*24); + } else { + vp._interval = (byte-192) * (3600*24*7); + } + + return vp; + + default: + throw new Error("Unknown VPF"); + } +}; + +/** + * getter for pdu message + * @return PDU + */ +VP.prototype.getPdu = function() +{ + return this._pdu; +}; + +/** + * set date time + * @param string datetime + */ +VP.prototype.setDateTime = function(datetime) +{ + this._datetime = new Date(Date.parse(datetime)); +}; + +/** + * set interval + * @param type interval + */ +VP.prototype.setInterval = function(interval) +{ + this._interval = interval; +}; + +/** + * cast to string + * @return string + */ +VP.prototype.toString = function() +{ + var SCTS = PDU.getModule('PDU/SCTS'), + Type = PDU.getModule('PDU/Type'); + + // get pdu type + var type = this.getPdu().getType(); + + // absolute value + if(this._datetime){ + type.setVpf(Type.VPF_ABSOLUTE); + return (new SCTS(this._datetime)).toString(); + } + + // relative value in seconds + if(this._interval){ + type.setVpf(Type.VPF_RELATIVE); + + var minutes = Math.ceil(this._interval / 60), + hours = Math.ceil(this._interval / 60 / 60), + days = Math.ceil(this._interval / 60 / 60 / 24), + weeks = Math.ceil(this._interval / 60 / 60 / 24 / 7); + + if(hours <= 12){ + return sprintf("%02X", Math.ceil(minutes/5)-1); + } else if(hours <= 24){ + return sprintf("%02X", Math.ceil((minutes-720)/30)+143); + } else if(hours <= (30*24*3600)) { + return sprintf("%02X", days+166); + } else { + return sprintf("%02X", (weeks > 63 ? 63 : weeks)+192); + } + } + + // vpf not used + type.setVpf(Type.VPF_NONE); + + return ""; +}; + + +module.exports = VP; \ No newline at end of file diff --git a/README.md b/README.md index d6f3512..4c9f6f4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ var pdu = require('pdu'); ``` -### pdu.generate() +### pdu.generate() +Depricated, but works ```js pdu.generate({ text:'Some text', @@ -27,6 +28,33 @@ var pdu = require('pdu'); returns an array of generated pdu's. +### pdu.Submit() +```js + var Submit = pdu.Submit(); // Submit, Deliver, Report + + // set number of sms center (optional) + Submit.setSca('999999999999'); + + // set number of recipent (required) + Submit.setAddress('+999999999999'); + + // set validity period 4 days (optional) + Submit.setVp(3600 * 24 * 4); + + // set text of message (required) + Submit.setData('Some text'); + + // set status report request (optional, default is off) + Submit.getType().setSrr(1); + + // get all parts of message + var parts = Submit.getParts(); + + parts.forEach(function(part){ + // part is object, instance of ./PDU/Data/Part, could be casted to string like ('' + part) or part.toString() + }); +``` + ### pdu.parse() ```js pdu.parse('06918919015000240C9189194238148900003110211052254117CAB03D3C1FCBD3703AA81D5E97E7A079D93D2FBB00'); @@ -42,3 +70,104 @@ encoding: '7bit', time: 1357953952000, text: 'Javascript makes sense.' } ``` +Note: current version of module supported object format and will merged with object of the pdu type (Submit, Deliver, Report) + +### pdu.parseStatusReport() +Will call method pdu.parse() and will create this format for legacy support +```js +{ smsc:smscNum, +reference:reference, +sender:sender, +status:status } +``` + +### PDU +* statics + * [Submit] Submit() // create object of Submit + * [Deliver] Deliver() // create object of Deliver + * [Report] Report() // create object of Report + * [void] debug(message) // if PDU.isDebug set true you will see debug information + * [Submit] generate() // legacy support + * [Submit|Deliver|Report] parse(str) // parse passwed string + * [Report] parseStatusReport() // legacy method, see parse() + +* object + * [SCA] getAddress() // get Originator or Destination Address + * [Data] getData() // get data + * [DCS] getDcs() // get Data Coding Scheme + * [array] getPars() // get parts of message, see the Part class + * [PID] getPid() // get Protoсol Identifier + * [SCA] getSca() // get Service Centre Address + * [Type] getType() // get Transport Protocol Data Unit + * [integer] getUdl() // get User Data Length + * [PDU] setAddress(SCA|String address) // set Originator or Destination Address + * [PDU] setData(Data|String) // set data message + * [PDU] setDcs(DCS|undefined dsc) // set Data Coding Scheme + * [PDU] setPid(PDI|undefined pid) // set Protoсol Identifier + * [PDU] setSca(SCA|String sca) // set Service Centre Address + + +### PDU/Submit - extended from PDU +* [integer] getMr() // get message reference +* [VP] getVp() // get validity period +* [void] setMr(integer mr) // set message reference +* [void] setVp(integer|string dtime) // set validity period + +### PDU/Deliver - extended from PDU +* [SCTS] getScts() // get time +* [void] setScts(SCTS|Date) // set time + +### PDU/Report - extended from PDU +* [SCTS] getDateTime() // get date time +* [SCTS] getDischarge() +* [integer] getReference() +* [integer] getStatus() +* [void] setDateTime(datetime) +* [void] setDischarge(discharge) +* [void] setReference(reference) +* [void] setStatus(status) + +### VP - Validity period +* [void] setDateTime(String datetime) // set date string fo Date.parse() +* [void] setInterval(Integer interval) // set interval in seconds + +### Type - Transport Protocol Data Unit + +* constants: + * SMS_SUBMIT = 0x01; + * SMS_DELIVER = 0x00; + * SMS_REPORT = 0x02; + * VPF_NONE = 0x00; + * VPF_SIEMENS = 0x01; + * VPF_RELATIVE = 0x02; + * VPF_ABSOLUTE = 0x03; + +* methods: + * [integer] getMti() + * [integer] getSrr() + * [integer] getUdhi() + * [integer] getValue() + * [integer] getVpf() + * [void] setSrr(Integer srr) + * [void] setUdhi(Integer udhi) + * [void] setVpf(Integer vpf) // one of constants VPF_{} + +### Type/Deliver +### Type/Report +### Type/Submit + +### SCTS - Time +* statics: + * [SCTS] parse(hex) // parse hex string +* methods: + * constructor(Date date) // + * [Integer] getTime() + +### SCA +### SCA/Type +### PID +### DCS +### Data +### Data/Header +### Data/Part +### Helper \ No newline at end of file diff --git a/Report.js b/Report.js new file mode 100644 index 0000000..68bb4fc --- /dev/null +++ b/Report.js @@ -0,0 +1,152 @@ +'use strict'; + +var PDU = require('./pdu'), + sprintf = require('sprintf'), + util = require('util'); + +function Report() +{ + + Report.super_.apply(this, arguments); + + /** + * referenced bytes + * @var integer + */ + this._reference; + + /** + * datetime + * @var SCTS + */ + this._timestamp; + + /** + * datetime + * @var SCTS + */ + this._discharge; + + /** + * report status + * 0x00 Short message received succesfully + * 0x01 Short message forwarded to the mobile phone, but unable to confirm delivery + * 0x02 Short message replaced by the service center + * 0x20 Congestion + * 0x21 SME busy + * 0x22 No response from SME + * 0x23 Service rejected + * 0x24 Quality of service not available + * 0x25 Error in SME + * 0x40 Remote procedure error + * 0x41 Incompatible destination + * 0x42 Connection rejected by SME + * 0x43 Not obtainable + * 0x44 Quality of service not available + * 0x45 No interworking available + * 0x46 SM validity period expired + * 0x47 SM deleted by originating SME + * 0x48 SM deleted by service center administration + * 0x49 SM does not exist + * 0x60 Congestion + * 0x61 SME busy + * 0x62 No response from SME + * 0x63 Service rejected + * 0x64 Quality of service not available + * 0x65 Error in SME + * + * @var integer + */ + this._status; +}; + +util.inherits(Report, PDU); + +/** + * set pdu type + * @param array $params + */ +Report.prototype.initType = function(params) +{ + var ReportType = require('./PDU/Type/Report'); + this._type = new ReportType(params || []); +}; + +/** + * get a referenced bytes + * @return integer + */ +Report.prototype.getReference = function() +{ + return this._reference; +}; + +/** + * setter for reference + * @param integer reference + */ +Report.prototype.setReference = function(reference) +{ + this._reference = reference; +}; + +/** + * + * @return SCTS + */ +Report.prototype.getDateTime = function() +{ + return this._timestamp; +}; + +/** + * setter timestamp + * @param string|int timestamp + */ +Report.prototype.setDateTime = function(timestamp) +{ + this._timestamp = timestamp; +}; + +/** + * + * @return SCTS + */ +Report.prototype.getDischarge = function() +{ + return this._discharge; +}; + +/** + * setter for discharge + * @param string|int discharge + */ +Report.prototype.setDischarge = function(discharge) +{ + this._discharge = discharge; +}; + +/** + * status report + * @return integer + */ +Report.prototype.getStatus = function() +{ + return this._status; +}; + +/** + * setter for status + * @param integer $status + */ +Report.prototype.setStatus = function(status) +{ + this._status = status; +}; + +Report.prototype.getStart = function() +{ + return null; +}; + +module.exports = Report; \ No newline at end of file diff --git a/Submit.js b/Submit.js new file mode 100644 index 0000000..9cb52d8 --- /dev/null +++ b/Submit.js @@ -0,0 +1,119 @@ +'use strict'; + +var PDU = require('./pdu'), + sprintf = require('sprintf'), + util = require('util'); + +function Submit() +{ + + Submit.super_.apply(this, arguments); + + /** + * Message Reference + * not changed for submit message + * @var integer + */ + this._mr = 0x00; + + /** + * Validity Period + * @var VP + */ + this._vp; + + this.setVp(); +}; + +util.inherits(Submit, PDU); + +/** + * set validity period + * @param string|int value + * @return Submit + */ +Submit.prototype.setVp = function(value) +{ + var VP = PDU.getModule('PDU/VP'); + + if(value instanceof VP){ + this._vp = value; + return this; + } + + this._vp = new VP(this); + + if(typeof(value) === 'string'){ + this._vp.setDateTime(value); + } else { + this._vp.setInterval(value); + } + + return this; +}; + +/** + * getter validity period + * @return VP + */ +Submit.prototype.getVp = function() +{ + return this._vp; +}; + +/** + * getter message reference + * @return integer + */ +Submit.prototype.getMr = function() +{ + return this._mr; +}; + +/** + * setter message reference + * @param integer mr + */ +Submit.prototype.setMr = function(mr) +{ + this._mr = mr; +}; + +/** + * set pdu type + * @param array params + * @return Submit + */ +Submit.prototype.initType = function(params) +{ + var SubmitType = require('./PDU/Type/Submit'); + this._type = new SubmitType(params || []); + return this; +}; + +/** + * Magic method for cast to string + * @return string + */ +Submit.prototype.toString = function() +{ + return this.getParts().map(function(part){ + return part.toString(); + }).join("\n"); +}; + +Submit.prototype.getStart = function() +{ + var str = ''; + str += this.getSca().toString(); + str += this.getType().toString(); + str += sprintf("%02X", this.getMr()); + str += this.getAddress().toString(); + str += sprintf("%02X", this.getPid().getValue()); + str += this.getDcs().toString(); + str += this.getVp().toString(); + + return str; +}; + +module.exports = Submit; \ No newline at end of file diff --git a/package.json b/package.json index 7fe28e4..23f541f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "A PDU parser and generator for browser and node", "homepage": "http://github.com/emilsedgh/pdu", "main": "./pdu", + "dependencies": { + "sprintf": "~0.1.5" + }, "keywords": [ "pdu", "sms" diff --git a/pdu.js b/pdu.js index 259f9fe..495b36c 100644 --- a/pdu.js +++ b/pdu.js @@ -1,381 +1,408 @@ -var pduParser = {}; +'use strict'; -pduParser.parse = function(pdu) { - //Cursor points to the last octet we've read. - var cursor = 0; +var util = require('util'), + sprintf = require('sprintf'); - var buffer = new Buffer(pdu.slice(0,4), 'hex'); - var smscSize = buffer[0]; - var smscType = buffer[1].toString(16); - cursor = (smscSize*2+2); - var smscNum = pduParser.deSwapNibbles(pdu.slice(4, cursor)); - - var buffer = new Buffer(pdu.slice(cursor,cursor+6), 'hex'); - cursor += 6; - var smsDeliver = buffer[0]; - - var smsDeliverBits = ("00000000"+parseInt(smsDeliver).toString(2)).slice(-8); - var udhi = smsDeliverBits.slice(1,2) === "1"; - - var senderSize = buffer[1]; - if(senderSize % 2 === 1) - senderSize++; - - var senderType = parseInt(buffer[2]).toString(16) - - - var senderNum = pduParser.deSwapNibbles(pdu.slice(cursor, cursor+senderSize)); - cursor += senderSize; - - var protocolIdentifier = pdu.slice(cursor, cursor+2); - cursor += 2; - - var dataCodingScheme = pdu.slice(cursor, cursor+2); - cursor = cursor+2; - - var encoding = pduParser.detectEncoding(dataCodingScheme); - - var timestamp = pduParser.deSwapNibbles(pdu.slice(cursor, cursor+14)); - - - var time = new Date; - time.setUTCFullYear('20'+timestamp.slice(0,2)); - time.setUTCMonth(timestamp.slice(2,4)-1); - time.setUTCDate(timestamp.slice(4,6)); - time.setUTCHours(timestamp.slice(6,8)); - time.setUTCMinutes(timestamp.slice(8,10)); - time.setUTCSeconds(timestamp.slice(10,12)); - - var firstTimezoneOctet = parseInt(timestamp.slice(12,13)); - var binary = ("0000"+firstTimezoneOctet.toString(2)).slice(-4); - var factor = binary.slice(0,1) === '1' ? 1 : -1; - var binary = '0'+binary.slice(1, 4); - var firstTimezoneOctet = parseInt(binary, 2).toString(10); - var timezoneDiff = parseInt(firstTimezoneOctet + timestamp.slice(13, 14)); - var time = new Date(time.getTime() + (timezoneDiff * 15 * 1000 * 60 * factor)); - - cursor += 14; - - var dataLength = parseInt(pdu.slice(cursor, cursor+2), 16).toString(10); - cursor += 2; - - if(udhi) { //User-Data-Header-Indicator: means there's some User-Data-Header. - var udhLength = pdu.slice(cursor, cursor+2); - var iei = pdu.slice(cursor+2, cursor+4); - if(iei == "00") { //Concatenated sms. - var headerLength = pdu.slice(cursor+4, cursor+6); - var referenceNumber = pdu.slice(cursor+6, cursor+8); - var parts = pdu.slice(cursor+8, cursor+10); - var currentPart = pdu.slice(cursor+10, cursor+12); - } - - if(iei == "08") { //Concatenaded sms with a two-bytes reference number - var headerLength = pdu.slice(cursor+4, cursor+6); - var referenceNumber = pdu.slice(cursor+6, cursor+10); - var parts = pdu.slice(cursor+10, cursor+12); - var currentPart = pdu.slice(cursor+12, cursor+14); - } - - if(encoding === '16bit') - if(iei == '00') - cursor += (udhLength-2)*4; - else if(iei == '08') - cursor += ((udhLength-2)*4)+2; - else - cursor += (udhLength-2)*2; - } - - if(encoding === '16bit') - var text = pduParser.decode16Bit(pdu.slice(cursor), dataLength); - else if(encoding === '7bit') - var text = pduParser.decode7Bit(pdu.slice(cursor), dataLength); - else if(encoding === '8bit') - var text = ''; //TODO - - var data = { - 'smsc' : smscNum, - 'smsc_type' : smscType, - 'sender' : senderNum, - 'sender_type' : senderType, - 'encoding' : encoding, - 'time' : time, - 'text' : text - }; - - if(udhi) { - data['udh'] = { - 'length' : udhLength, - 'iei' : iei, - }; - - if(iei == '00' || iei == '08') { - data['udh']['reference_number'] = referenceNumber; - data['udh']['parts'] = parseInt(parts); - data['udh']['current_part'] = parseInt(currentPart); - } - } - - return data; -} - -pduParser.detectEncoding = function(dataCodingScheme) { - var binary = ('00000000'+(parseInt(dataCodingScheme, 16).toString(2))).slice(-8); - - if(binary == '00000000') - return '7bit'; - - if(binary.slice(0, 2) === '00') { - var compressed = binary.slice(2, 1) === '1'; - var bitsHaveMeaning = binary.slice(3, 1) === '1'; +function PDU() +{ + /** + * Service Centre Address + * @var SCA + */ + this._sca; + + /** + * Transport Protocol Data Unit + * @var Type + */ + this._type; + + /** + * Originator or Destination Address + * @var SCA + */ + this._address; + + /** + * Protoсol Identifier + * @var PID + */ + this._pid; + + /** + * Data Coding Scheme + * @var DCS + */ + this._dcs; + + /** + * User Data Length + * @var integer + */ + this._udl; + + /** + * User Data + * @var string + */ + this._ud; + + this.setSca(); + this.initType(); + this.setPid(); + this.setDcs(); + +}; + +PDU.getModule = function(name) +{ + return require('./' + name); +}; - if(binary.slice(4,6) === '00') - return '7bit'; +PDU.Submit = function() +{ + return new (require('./Submit'))(); +}; - if(binary.slice(4,6) === '01') - return '8bit'; +PDU.Report = function() +{ + return new (require('./Report'))(); +}; - if(binary.slice(4,6) === '10') - return '16bit'; +PDU.Deliver = function() +{ + return new (require('./Deliver'))(); +}; + +/** + * Legasy support + * @returns {array} + */ +PDU.generate = function(params) +{ + + if( ! params.receiver){ + throw new Error("Receiver not set"); } -} - -pduParser.decode16Bit = function(data, length) { - //We are getting ucs2 characters. - var ucs2 = ''; - for(var i = 0;i<=data.length;i=i+4) { - ucs2 += String.fromCharCode("0x"+data[i]+data[i+1]+data[i+2]+data[i+3]); + + var DCS = PDU.getModule('PDU/DCS'); + + var Submit = PDU.Submit(), + dcs = Submit.getDcs(); + + switch(params.encoding){ + case '16bit': dcs.setTextAlphabet(DCS.ALPHABET_UCS2); break; + case '8bit': dcs.setTextAlphabet(DCS.ALPHABET_8BIT); break; + case '7bit': dcs.setTextAlphabet(DCS.ALPHABET_DEFAULT); break; } - - return ucs2; -} - -pduParser.deSwapNibbles = function(nibbles) { - var out = ''; - for(var i = 0; i< nibbles.length; i=i+2) { - if(nibbles[i] === 'F') //Dont consider trailing F. - out += parseInt(nibbles[i+1], 16).toString(10); - else - out += parseInt(nibbles[i+1], 16).toString(10)+parseInt(nibbles[i], 16).toString(10); + + Submit.setAddress('' + params.receiver); + Submit.setData(params.text || ''); + Submit.getType().setSrr(1); + + var parts = Submit.getParts(); + + return parts.map(function(part){ + return part.toString(); + }); + +}; + +/** + * Legacy support + * @param {string} str + * @returns {PDU} + */ +PDU.parseStatusReport = function(str) +{ + var pdu = PDU.parse(str); + + pdu.smsc = pdu.getSca().getPhone(); + pdu.reference = pdu.getReference(); + pdu.sender = pdu.getSca().getPhone(); + pdu.status = pdu.getStatus(); + + return pdu; +}; + +/** + * parsed string + * @var string + */ +PDU._pduParse = ''; + +/** + * get a part pdu string and cut them from pdu + * @param integer length + * @return string + */ +PDU.getPduSubstr = function(length) +{ + var str = PDU._pduParse.substr(0, length); + PDU._pduParse = PDU._pduParse.substr(length); + return str; +}; + +/** + * parse pdu string + * @param string str + * @return PDU + * @throws Error + */ +PDU.parse = function(str) +{ + var SCA = PDU.getModule('PDU/SCA'), + DCS = PDU.getModule('PDU/DCS'), + Deliver = require('./Deliver'), + Helper = PDU.getModule('PDU/Helper'); + + // current pdu string + PDU._pduParse = str; + + // parse service center address + var sca = SCA.parse(false); + + // parse type of sms + var self = Helper.getPduByType(); + + // set sca + self._sca = sca; + + // parse sms address + self._address = SCA.parse(); + + self = Helper.initVars(self); + + // Legacy support + self.smsc = self.getSca().getPhone(); + self.smsc_type = self.getSca().getType().toString(); + self.sender = self.getAddress().getPhone(); + self.sender_type = self.getAddress().getType().toString(); + + if(self.getData()){ + self.text = self.getData().getData(); } - return out; -} - -pduParser.decode7Bit = function(code, count) { - //We are getting 'septeps'. We should decode them. - var binary = ''; - for(var i = 0; i 0) - hex += ('00'+(parseInt(octets[i], 2).toString(16))).slice(-2); - return hex; -} - -//TODO: TP-Validity-Period (Delivery) -pduParser.generate = function(message) { - var pdu = '00'; - - var parts = 1; - if(message.encoding === '16bit' && message.text.length > 70) - parts = message.text.length / 66; - - else if(message.encoding === '7bit' && message.text.length > 160) - parts = message.text.length / 153; - - parts = Math.ceil(parts); - - TPMTI = 1; - TPRD = 4; - TPVPF = 8; - TPSRR = 32; - TPUDHI = 64; - TPRP = 128; - - var submit = TPMTI; - - if(parts > 1) //UDHI - submit = submit | TPUDHI; - - submit = submit | TPSRR; - - pdu += submit.toString(16); - - pdu += '00'; //TODO: Reference Number; - - var receiverSize = ('00'+(parseInt(message.receiver.length, 10).toString(16))).slice(-2); - var receiver = pduParser.swapNibbles(message.receiver); - var receiverType = 81; //TODO: NOT-Hardcoded PDU generation. Please note that Hamrah1 doesnt work if we set it to 91 (International). - - pdu += receiverSize.toString(16) + receiverType + receiver; - - pdu += '00'; //TODO TP-PID - - if(message.encoding === '16bit') - pdu += '08'; - else if(message.encoding === '7bit') - pdu += '00'; - - var pdus = new Array(); - - var csms = randomHexa(2); // CSMS allows to give a reference to a concatenated message - - for(var i=0; i< parts; i++) { - pdus[i] = pdu; - - if(message.encoding === '16bit') { - /* If there are more than one messages to be sent, we are going to have to put some UDH. Then, we would have space only - * for 66 UCS2 characters instead of 70 */ - if(parts === 1) - var length = 70; - else - var length = 66; - - } else if(message.encoding === '7bit') { - /* If there are more than one messages to be sent, we are going to have to put some UDH. Then, we would have space only - * for 153 ASCII characters instead of 160 */ - if(parts === 1) - var length = 160; - else - var length = 153; - } - var text = message.text.slice(i*length, (i*length)+length); - - if(message.encoding === '16bit') { - user_data = pduParser.encode16Bit(text); - var size = (user_data.length / 2); - - if(parts > 1) - size += 6; //6 is the number of data headers we append. - - } else if(message.encoding === '7bit') { - user_data = pduParser.encode7Bit(text); - var size = user_data.length / 2; - } - - pdus[i] += ('00'+parseInt(size).toString(16)).slice(-2); - - if(parts > 1) { - pdus[i] += '05'; - pdus[i] += '00'; - pdus[i] += '03'; - pdus[i] += csms; - pdus[i] += ('00'+parts.toString(16)).slice(-2); - pdus[i] += ('00'+(i+1).toString(16)).slice(-2); - } - pdus[i] += user_data; + if( ! this._sca){ + this._sca = new SCA(false); } - return pdus; -} + if(address){ + this._sca.setPhone(address, true); + } + return this; +}; -pduParser.encode16Bit = function(text) { - var out = ''; - for(var i = 0; i