UNPKG

node-sms-pdu

Version:

The node-sms-pdu is a SMS-SUBMIT PDU (Packet Data Unit) generator and SMS-SUBMIT/DELIVER PDU parser. This module supports the GSM 7-bit default alphabet encoding and the UCS-2 16-bit alphabet encoding. Besides, it supports the Concatenated (or Multipart o

756 lines (648 loc) 20.1 kB
/* ------------------------------------------------------------------ * node-sms-pdu - sms-pdu.js * * Copyright (c) 2020, Futomi Hatano, All rights reserved. * Released under the MIT license * Date: 2020-10-29 * ---------------------------------------------------------------- */ 'use strict'; const mSmsPduUserDataGsm0338 = require('./sms-pdu-user-data-gsm0338.js'); const mSmsPduUserDataUcs2 = require('./sms-pdu-user-data-ucs2.js'); class SmsPdu { /* ------------------------------------------------------------------ * Constructor * * [Arguments] * - None * ---------------------------------------------------------------- */ constructor() { } /* ------------------------------------------------------------------ * generateSubmit(number, text, options) * - Create a SMS PDU. * * [Arguments] * - number | String | Required | Mobile phone number * | | | - Example: * | | | - national: "09012345678" * | | | - international: "+819012345678" * | | | - "-" in the number is acceptable but it is ignored. * - text | String | Required | Message text * - options | Object | Optional | * - encoding | Integer | Optional | Encoding type: * | | | - "gsm" : GSM 7 bit default alphabet * | | | - "ucs2": 16 bit alphabet (UCS-2) * * [Returen value] * [ * { * "buffer" : <Buffer 00...>, // Buffer object of the PDU * "hex" : "0001000B81.."", // HEX string of the PDU * "length" : 21, // Byte length without the SCA (Service Center Address) * "encoding": "gsm" // Encoding type ("gsm" or "ucs2") * }, * ... * ] * ---------------------------------------------------------------- */ generateSubmit(number, text, options) { // Check the parameter `number` if (!number || typeof (number) !== 'string' || /^\+?[0-9\-]+$/.test(number) === false) { throw new Error('The `number` is invalid.'); } number = number.replace(/\-/g, ''); let international = false; if (/^\+/.test(number)) { international = true; number = number.replace(/^\+/, ''); } // Check the parameter `text` if (!text || typeof (text) !== 'string' || text.length === 0) { throw new Error('The `text` must be a non-empty string.'); } let byte_list = []; // Check the parameter `options` if (options) { if (typeof (options) !== 'object') { throw new Error('The `options` must be an object.'); } } else { options = {}; } // Check the parameter `encoding` let encoding = ''; if ('encoding' in options) { if (options.encoding) { encoding = options.encoding; if (typeof (encoding) !== 'string' || /^(gsm|ucs2)$/.test(encoding) === false) { throw new Error('The `encoding` must be "gsm" or "ucs2".'); } } } if (!encoding) { if (mSmsPduUserDataGsm0338.isAcceptable(text)) { encoding = 'gsm'; } else { encoding = 'ucs2'; } } // Create UD (User Data) list let ud_list = []; if (encoding === 'gsm') { // GSM 7-bit default alphabet ud_list = mSmsPduUserDataGsm0338.encode(text); } else if (encoding === 'ucs2') { // 16 bit alphabet (UCS-2) ud_list = mSmsPduUserDataUcs2.encode(text); } // SCA ( Service Center address) byte_list.push(0x00); let sca_byte_len = 1; // Protocol Data Unit Type (PDU Type) // - UDHI (bit 6): // - 0: User Data field contains only the short message // - 1: The beginning of the UD field contains a Header in addition to the short message // - MTI (bit 1, 0): // - 01: SMS-SUBMIT to SMSC, VP field is not present let udhi = (ud_list.length > 1) ? 1 : 0; let mti = 0b01; let pdu_type = (udhi << 6) | mti; byte_list.push(pdu_type); // MR (Message Reference) byte_list.push(0x00); // DA (Destination Address) length let da_len = number.length; byte_list.push(da_len); // DA type of number (0x81: national, 0x91: international) if (international) { byte_list.push(0x91); // international } else { byte_list.push(0x81); // national } // DA BCD-digits let da_nlist = number.split(''); if (da_nlist.length % 2 === 1) { da_nlist.push('f'); } for (let i = 0; i < da_nlist.length; i += 2) { let n0 = parseInt(da_nlist[i], 16); let n1 = parseInt(da_nlist[i + 1], 16); let n = (n1 << 4) + n0; byte_list.push(n); } // PID (Protocol Identifier) let pid = 0x00; // The PDU has to be treat as a short message byte_list.push(pid); // DCS (Data Coding Scheme) if (encoding === 'gsm') { // GSM 7-bit default alphabet byte_list.push(0x00); } else if (encoding === 'ucs2') { // 16 bit alphabet (UCS-2) byte_list.push(0x08); } else { throw new Error('The `text` can not be encoded.'); } // VP (Validity Period) // - not supported // Create a PDU buffer object let buf = Buffer.from(byte_list); let pdu_list = []; for (let ud of ud_list) { // UDL (User Data Length) let udl_buf = Buffer.from([ud.length]); let ud_buf = ud.buffer; let pdu_buf = Buffer.concat([buf, udl_buf, ud_buf]); pdu_list.push({ buffer: pdu_buf, hex: pdu_buf.toString('hex').toUpperCase(), length: pdu_buf.length - sca_byte_len, encoding: encoding }); } return pdu_list; } /* ------------------------------------------------------------------ * getEncoding(number, text) * - Get the encoding type * * [Arguments] * - text | String | Required | Message text * * [Returen value] * - "gsm" : GSM 7 bit default alphabet * - "ucs2": 16 bit alphabet (UCS-2) * ---------------------------------------------------------------- */ getEncoding(text) { if (mSmsPduUserDataGsm0338.isAcceptable(text)) { return 'gsm'; } else { return 'ucs2'; } } /* ------------------------------------------------------------------ * parse(data) * - Parse a SMS PDU data. * * [Arguments] * - data | String | Required | HEX string of a SMS PDU or * | Buffer | | Buffer object of a SMS PDU * * [Returen value] * ---------------------------------------------------------------- */ parse(data) { // Check the `data` let buf = null; if (typeof (data) === 'string') { data = data.toLowerCase(); if (/^[a-f0-9]+$/.test(data) === false || data.length === 0 || data.length % 2 !== 0) { return { error: new Error('The `data` must be hexadecimal representation.') }; } let byte_list = []; for (let i = 0; i < data.length; i += 2) { let h = data.substring(i, i + 2); let n = parseInt(h, 16); byte_list.push(n); } buf = Buffer.from(byte_list); } else if (Buffer.isBuffer(data)) { if (data.length === 0) { return { error: new Error('The `data` must be a non-empty data.') }; } buf = data; } else { return { error: new Error('The `data` must be a string or an Buffer object.') }; } // Parse the data let details = null; try { details = this._parseDetails(buf); } catch (e) { return { error: e }; } // Compose the response let res = {}; let smsc = null; if (details.sca.digit > 0) { smsc = details.sca.address; if (details.sca.international) { smsc = '+' + smsc; } } res.smsc = smsc; if (details.pduType.mti === 0) { res.type = 'SMS-DELIVER'; if (details.oa && details.oa.digit > 0) { let val = details.oa.address; if (details.oa.international) { val = '+' + val; } res.origination = val; } else { res.origination = null; } if (details.scts) { res.timestamp = details.scts; } else { res.timestamp = null; } } else if (details.pduType.mti === 1) { res.type = 'SMS-SUBMIT'; res.reference = details.mr; if (details.da && details.da.digit > 0) { let val = details.da.address; if (details.da.international) { val = '+' + val; } res.destination = val; } else { res.destination = null; } if (details.vp) { res.period = details.vp.period + details.vp.unit; } else { res.period = null; } } else { return { error: new Error('Unknown PDU Type: MTI=' + details.pduType.mti) }; } if (details.ud.udh) { res.concat = { reference: details.ud.udh.reference, total: details.ud.udh.total, sequence: details.ud.udh.sequence }; } else { res.concat = null; } if (details.ud.text) { res.text = details.ud.text; } else { res.text = null; } res.details = details; return res; } _parseDetails(buf) { let sca = this._parseSca(buf); let offset = sca.length; if (buf.length < offset + 1) { throw new Error('The length of the data is insufficient.'); } let pdu_type = this._parsePduType(buf.slice(offset, offset + 1)); offset += 1; let res1 = { sca: sca, pduType: pdu_type }; let res2 = {}; if (pdu_type.mti === 0) { // SMS-DELIVER res2 = this._parseSmsDeliver(buf.slice(offset)); offset += res2.length; delete res2.length; } else if (pdu_type.mti === 1) { // SMS-SUBMIT res2 = this._parseSmsSubmit(buf.slice(offset), pdu_type.vpf); offset += res2.length; delete res2.length; } else { throw new Error('The message type is not supported: MTI=' + pdu_type.mti); } let res = Object.assign(res1, res2); // UDL: User Data Length let udl = buf.readUInt8(offset); res.udl = udl; offset += 1; if (res.dcs.alphabet === 0) { if (buf.length !== offset + Math.ceil(udl * 7 / 8)) { throw new Error('The length of the data is insufficient.'); } } else { if (buf.length !== offset + udl) { throw new Error('The length of the data is insufficient.'); } } // UD: User Data res.ud = {}; // UDH: User Data Header let ud_buf = buf.slice(offset); let udh = null; let udh_len = 0; if (res.pduType.udhi === true) { udh = this._parseUdh(ud_buf); udh_len = udh.length; delete udh.length; } res.ud.udh = udh; if (buf.length === offset + udh_len) { return res; } // Data if (res.dcs.alphabet === 0) { // Default alphabet (7bit) res.ud.text = mSmsPduUserDataGsm0338.decode(ud_buf, udh_len); } else if (res.dcs.alphabet === 2) { // UCS2 (16bit) res.ud.text = mSmsPduUserDataUcs2.decode(ud_buf, udh_len); } else { res.ud.hex = ud_buf.toString('hex').toUpperCase() } return res; } _parseSca(buf) { let len = buf.readUInt8(0) + 1; if (len === 1) { return { length: 1, digit: 0, international: false, address: '' }; } if (buf.length < len) { throw new Error('The length of the data is insufficient.'); } let type = buf.readUInt8(1); let international = (type === 0x91) ? true : false; let address = ''; for (let i = 2; i < len; i++) { let hex = buf.slice(i, i + 1).toString('hex'); address += hex.substring(1, 2); address += hex.substring(0, 1); } address = address.replace(/f$/, ''); return { length: len, digit: address.length, international: international, address: address }; } _parsePduType(buf) { let n = buf.readUInt8(0); // RP: Whether the reply Path parameter is set or not in this SMS-SUBMIT let rp = (n & 0b10000000) ? true : false; // UDHI: Whether the UD (User Data) contains a UDH (User Data Header) or not let udhi = (n & 0b01000000) ? true : false; // SRR: Status Report Request (A status report is requested or not) (For SMS-SUBMIT) // SRI: Status Report Indicator (A status report will be returned to the SME or not) (For SMS-DELIVER) let srr = (n & 0b00100000) ? true : false; let sri = srr; // VPF: Validity Period Format (For SMS-SUBMIT) // - 0 (0b00): Validity Period not present // - 1 (0b01): Validity Period present - enhanced format (reserved) // - 2 (0b10): Validity Period present- relative format // - 3 (0b00): Validity Period present - absolute format let vpf = (n & 0b00011000) >>> 3; // RD: Reject Duplicates (For SMS-SUBMIT) // MMS: More Message to Send (For SMS-DELIVER) let rd = (n & 0b00000100) ? true : false; let mms = rd; // MTI: Message Type Indicator // - 0 (0b00): SMS-DELIVER (in the direction SC to MS) // SMS-DELIVER REPORT (in the direction MS to SC) // - 1 (0b01): SMS-SUBMIT (in the direction MS to SC) // SMS-SUBMIT-REPORT (in the direction SC to MS) // - 2 (0b02): SMS-STATUS-REPORT (in the direction SC to MS) // SMS-COMMAND (in the direction MS to SC) let mti = n & 0b00000011; let res = { rp: rp, udhi: udhi, vpf: vpf, mti: mti }; if (mti === 0) { // SMS-SUBMIT res.srr = srr; res.rd = rd; return res; } else if (mti === 1) { // SMS-DELIVER res.sri = sri; res.mms = mms; return res; } else { throw new Error('The message type is not supported: MTI=' + mti); } } _parseSmsSubmit(buf, vpf) { if (buf.length < 8) { throw new Error('The length of the data is insufficient.'); } // MR: Message Reference let mr = buf.readUInt8(0); let offset = 1; // DA: Destination Address let da = this._parseAddress(buf.slice(1)); offset += da.length; if (buf.length < offset + 3) { throw new Error('The length of the data is insufficient.'); } // PID: Protocol Identifier let pid = buf.readUInt8(offset); offset += 1; // DCS: Data Coding Scheme let dcs = this._parseDcs(buf.slice(offset, offset + 1)); offset += 1; // VP: Validity Period let vp = null; if (vpf === 0b10) { if (buf.length < offset + 2) { throw new Error('The length of the data is insufficient.'); } vp = this._parseVpRelative(buf.slice(offset, offset + 1)); offset += 1; } else if (vpf === 0b11) { if (buf.length < offset + 8) { throw new Error('The length of the data is insufficient.'); } vp = this._parseVpAbsolute(buf.slice(offset, offset + 7)); offset += 7; } return { length: offset, mr: mr, da: da, pid: pid, dcs: dcs, vp: vp }; } _parseSmsDeliver(buf) { if (buf.length < 11) { throw new Error('The length of the data is insufficient.'); } let offset = 0; // OA: Origination Address let oa = this._parseAddress(buf); offset += oa.length; if (buf.length < offset + 9) { throw new Error('The length of the data is insufficient.'); } // PID: Protocol Identifier let pid = buf.readUInt8(offset); offset += 1; // DCS: Data Coding Scheme let dcs = this._parseDcs(buf.slice(offset, offset + 1)); offset += 1; // SCTS: Service Center Time Stamp let scts = this._parseScts(buf.slice(offset, offset + 7)); offset += 7; return { length: offset, oa: oa, pid: pid, dcs: dcs, scts: scts }; } _parseAddress(buf) { let digit = buf.readUInt8(0); if (digit === 0) { return { length: 1, digit: 0, international: false, address: '' }; } let len = Math.ceil(digit / 2) + 2; if (buf.length < len) { throw new Error('The length of the data is insufficient.'); } let address_type = buf.readUInt8(1); let number_type = (address_type & 0b01110000) >>> 4; let international = (number_type === 0b001) ? true : false; let address = ''; if (number_type === 0b101) { // Alphanumeric, (coded according to GSM TS 03.38 7-bit default alphabet) address = mSmsPduUserDataGsm0338.decode(buf.slice(2, len), 0); } else { for (let i = 2; i < len; i++) { let hex = buf.slice(i, i + 1).toString('hex'); address += hex.substring(1, 2); address += hex.substring(0, 1); } address = address.replace(/f$/, ''); } return { length: len, digit: digit, international: international, type: number_type, address: address }; } _parseDcs(buf) { let n = buf.readUInt8(0); let res = {}; // Compessed if ((n >>> 6) === 0b00) { res.compressed = (n & 0b00100000) ? true : false; } else { res.compressed = false; } // Message Class if (n & 0b00010000) { // - 0 (0b00): Class 0 (immediate display) // - 1 (0b01): Class 1 (ME specific) // - 2 (0b10): Class 2 (SIM specific) // - 3 (0b11): Class 3 (TE specific) res.mclass = (n & 0b00000011); } else { res.mclass = -1; } // Alphabet // - 0 (0b00): Default alphabet (7bit) // - 1 (0b01): 8 bit data // - 2 (0b10): UCS2 (16bit) res.alphabet = (n & 0b00001100) >>> 2; return res; } _parseVpRelative(buf) { let vpn = buf.readUInt8(0); let vp = {}; if (vpn <= 143) { vp = { period: (vpn + 1) * 5, unit: 'm' // Minutes }; } else if (vpn <= 167) { vp = { period: (12 * 60) + ((vpn - 143) * 30), unit: 'm' // Minites }; } else if (vpn <= 196) { vp = { period: vpn - 166, unit: 'd' // Days }; } else { vp = { period: vpn - 192, unit: 'w' // Weeks }; } return vp; } _parseVpAbsolute(buf) { let ts = this._parseScts(buf); return { datetime: ts }; } _parseScts(buf) { let digits = buf.toString('hex').split(''); let Y = parseInt(digits[1] + digits[0], 10); if (Y > (new Date).getFullYear() % 100) { Y += 1900; } else { Y += 2000; } Y = Y.toString(); let M = digits[3] + digits[2]; let D = digits[5] + digits[4]; let h = digits[7] + digits[6]; let m = digits[9] + digits[8]; let s = digits[11] + digits[10]; let tz_mins = parseInt(digits[13] + digits[12], 10) * 15; let tz_h = ('0' + Math.floor(tz_mins / 60)).slice(-2); let tz_m = ('0' + (tz_mins % 60)).slice(-2); let date = [Y, M, D].join('-'); let time = [h, m, s].join(':'); let ts = date + 'T' + time + '+' + tz_h + ':' + tz_m; return ts; } _parseUdh(buf) { // Length of User Data Header let len = buf.readUInt8(0); let byte_len = len + 1; if (buf.length < byte_len) { throw new Error('The length of the data is insufficient.'); } // Information Element Identifier let iei = buf.readUInt8(1); if (iei !== 0x00) { throw new Error('The Information Element Identifier in the UDH is not supported: IEI=' + iei); } // Length of the header, excluding the first two fields let hlen = buf.readUInt8(2); if (buf.length < hlen + 2) { throw new Error('The length of the data is insufficient.'); } // CSMS reference number let reference = buf.readUInt8(3); // Total number of parts let total = buf.readUInt8(4); // This part's number in the sequence let seq = buf.readUInt8(5); return { length: byte_len, iei: iei, reference: reference, total: total, sequence: seq }; } } module.exports = new SmsPdu();