| 1 | /** |
| 2 | * @description MeshCentral SMS gateway communication module |
| 3 | * @author Ylian Saint-Hilaire |
| 4 | * @copyright Intel Corporation 2018-2022 |
| 5 | * @license Apache-2.0 |
| 6 | * @version v0.0.1 |
| 7 | */ |
| 8 | |
| 9 | /*xjslint node: true */ |
| 10 | /*xjslint plusplus: true */ |
| 11 | /*xjslint maxlen: 256 */ |
| 12 | /*jshint node: true */ |
| 13 | /*jshint strict: false */ |
| 14 | /*jshint esversion: 6 */ |
| 15 | "use strict"; |
| 16 | |
| 17 | /* |
| 18 | // For Twilio, add this in config.json |
| 19 | "sms": { |
| 20 | "provider": "twilio", |
| 21 | "sid": "ACxxxxxxxxx", |
| 22 | "auth": "xxxxxxx", |
| 23 | "from": "+15555555555" |
| 24 | }, |
| 25 | |
| 26 | // For Plivo, add this in config.json |
| 27 | "sms": { |
| 28 | "provider": "plivo", |
| 29 | "id": "xxxxxxx", |
| 30 | "token": "xxxxxxx", |
| 31 | "from": "15555555555" |
| 32 | } |
| 33 | |
| 34 | // For Telnyx, add this in config.json |
| 35 | "sms": { |
| 36 | "provider": "telnyx", |
| 37 | "apikey": "xxxxxxx", |
| 38 | "from": "15555555555" |
| 39 | } |
| 40 | |
| 41 | // For URL, add this in config.json |
| 42 | "sms": { |
| 43 | "provider": "url", |
| 44 | "url": "https://sample.com/?phone={{phone}}&msg={{message}}" |
| 45 | } |
| 46 | */ |
| 47 | |
| 48 | // Construct a SMS server object |
| 49 | module.exports.CreateMeshSMS = function (parent) { |
| 50 | var obj = {}; |
| 51 | obj.parent = parent; |
| 52 | obj.provider = null; |
| 53 | |
| 54 | // SMS gateway provider setup |
| 55 | switch (parent.config.sms.provider) { |
| 56 | case 'twilio': { |
| 57 | // Validate Twilio configuration values |
| 58 | if (typeof parent.config.sms.sid != 'string') { console.log('Invalid or missing SMS gateway provider sid.'); return null; } |
| 59 | if (typeof parent.config.sms.auth != 'string') { console.log('Invalid or missing SMS gateway provider auth.'); return null; } |
| 60 | if (typeof parent.config.sms.from != 'string') { console.log('Invalid or missing SMS gateway provider from.'); return null; } |
| 61 | |
| 62 | // Setup Twilio |
| 63 | var Twilio = require('twilio'); |
| 64 | obj.provider = new Twilio(parent.config.sms.sid, parent.config.sms.auth); |
| 65 | break; |
| 66 | } |
| 67 | case 'plivo': { |
| 68 | // Validate Plivo configuration values |
| 69 | if (typeof parent.config.sms.id != 'string') { console.log('Invalid or missing SMS gateway provider id.'); return null; } |
| 70 | if (typeof parent.config.sms.token != 'string') { console.log('Invalid or missing SMS gateway provider token.'); return null; } |
| 71 | if (typeof parent.config.sms.from != 'string') { console.log('Invalid or missing SMS gateway provider from.'); return null; } |
| 72 | |
| 73 | // Setup Plivo |
| 74 | var plivo = require('plivo'); |
| 75 | obj.provider = new plivo.Client(parent.config.sms.id, parent.config.sms.token); |
| 76 | break; |
| 77 | } |
| 78 | case 'telnyx': { |
| 79 | // Validate Telnyx configuration values |
| 80 | if (typeof parent.config.sms.apikey != 'string') { console.log('Invalid or missing SMS gateway provider apikey.'); return null; } |
| 81 | if (typeof parent.config.sms.from != 'string') { console.log('Invalid or missing SMS gateway provider from.'); return null; } |
| 82 | |
| 83 | // Setup Telnyx |
| 84 | obj.provider = require('telnyx')(parent.config.sms.apikey); |
| 85 | break; |
| 86 | } |
| 87 | case 'url': { |
| 88 | // Validate URL configuration values |
| 89 | if (parent.config.sms.url != 'console') { |
| 90 | if (typeof parent.config.sms.url != 'string') { console.log('Invalid or missing SMS gateway URL value.'); return null; } |
| 91 | if (!parent.config.sms.url.toLowerCase().startsWith('http://') && !parent.config.sms.url.toLowerCase().startsWith('https://')) { console.log('Invalid or missing SMS gateway, URL must start with http:// or https://.'); return null; } |
| 92 | if (parent.config.sms.url.indexOf('{{message}}') == -1) { console.log('Invalid or missing SMS gateway, URL must include {{message}}.'); return null; } |
| 93 | if (parent.config.sms.url.indexOf('{{phone}}') == -1) { console.log('Invalid or missing SMS gateway, URL must include {{phone}}.'); return null; } |
| 94 | } |
| 95 | break; |
| 96 | } |
| 97 | default: { |
| 98 | // Unknown SMS gateway provider |
| 99 | console.log('Unknown SMS gateway provider: ' + parent.config.sms.provider); |
| 100 | return null; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // Send an SMS message |
| 105 | obj.sendSMS = function (to, msg, func) { |
| 106 | parent.debug('email', 'Sending SMS to: ' + to + ': ' + msg); |
| 107 | if (parent.config.sms.provider == 'twilio') { // Twilio |
| 108 | obj.provider.messages.create({ |
| 109 | from: parent.config.sms.from, |
| 110 | to: to, |
| 111 | body: msg |
| 112 | }, function (err, result) { |
| 113 | if (err != null) { parent.debug('email', 'SMS error: ' + err.message); } else { parent.debug('email', 'SMS result: ' + JSON.stringify(result)); } |
| 114 | if (func != null) { func((err == null) && (result.status == 'queued'), err ? err.message : null, result); } |
| 115 | }); |
| 116 | } else if (parent.config.sms.provider == 'plivo') { // Plivo |
| 117 | if (to.split('-').join('').split(' ').join('').split('+').join('').length == 10) { to = '1' + to; } // If we only have 10 digits, add a 1 in front. |
| 118 | obj.provider.messages.create( |
| 119 | parent.config.sms.from, |
| 120 | to, |
| 121 | msg |
| 122 | ).then(function (result) { |
| 123 | parent.debug('email', 'SMS result: ' + JSON.stringify(result)); |
| 124 | if (func != null) { func((result != null) && (result.messageUuid != null), null, result); } |
| 125 | } |
| 126 | ).catch(function (err) { |
| 127 | var msg = null; |
| 128 | if ((err != null) && err.message) { msg = JSON.parse(err.message).error; } |
| 129 | parent.debug('email', 'SMS error: ' + msg); |
| 130 | if (func != null) { func(false, msg, null); } |
| 131 | } |
| 132 | ); |
| 133 | } else if (parent.config.sms.provider == 'telnyx') { // Telnyx |
| 134 | obj.provider.messages.create({ |
| 135 | from: parent.config.sms.from, |
| 136 | to: to, |
| 137 | text: msg |
| 138 | }, function (err, result) { |
| 139 | if (err != null) { parent.debug('email', 'SMS error: ' + err.type); } else { parent.debug('email', 'SMS result: ' + JSON.stringify(result)); } |
| 140 | if (func != null) { func((err == null), err ? err.type : null, result); } |
| 141 | }); |
| 142 | } else if (parent.config.sms.provider == 'url') { // URL |
| 143 | if (parent.config.sms.url == 'console') { |
| 144 | // This is for debugging, just display the SMS to the console |
| 145 | console.log('SMS (' + to + '): ' + msg); |
| 146 | if (func != null) { func(true, null, null); } |
| 147 | } else { |
| 148 | var sms = parent.config.sms.url.split('{{phone}}').join(encodeURIComponent(to)).split('{{message}}').join(encodeURIComponent(msg)); |
| 149 | parent.debug('email', 'SMS URL: ' + sms); |
| 150 | sms = new URL(sms); |
| 151 | if (sms.protocol == 'https:') { |
| 152 | // HTTPS GET request |
| 153 | const options = { hostname: sms.hostname, port: sms.port ? sms.port : 443, path: sms.pathname + sms.search, method: 'GET', rejectUnauthorized: false }; |
| 154 | const request = require('https').request(options, function (res) { parent.debug('email', 'SMS result: ' + res.statusCode); if (func != null) { func(res.statusCode == 200, (res.statusCode == 200) ? null : res.statusCode, null); } res.on('data', function (d) { }); }); |
| 155 | request.on('error', function (err) { parent.debug('email', 'SMS error: ' + err); if (func != null) { func(false, err, null); } }); |
| 156 | request.end(); |
| 157 | } else { |
| 158 | // HTTP GET request |
| 159 | const options = { hostname: sms.hostname, port: sms.port ? sms.port : 80, path: sms.pathname + sms.search, method: 'GET' }; |
| 160 | const request = require('http').request(options, function (res) { parent.debug('email', 'SMS result: ' + res.statusCode); if (func != null) { func(res.statusCode == 200, (res.statusCode == 200) ? null : res.statusCode, null); } res.on('data', function (d) { }); }); |
| 161 | request.on('error', function (err) { parent.debug('email', 'SMS error: ' + err); if (func != null) { func(false, err, null); } }); |
| 162 | request.end(); |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | // Get the correct SMS template |
| 169 | function getTemplate(templateNumber, domain, lang) { |
| 170 | parent.debug('email', 'Getting SMS template #' + templateNumber + ', lang: ' + lang); |
| 171 | if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given. |
| 172 | if (lang != null) { lang = lang.split('-')[0]; } // Take the first part of the language, "xx-xx" |
| 173 | |
| 174 | var r = {}, emailsPath = null; |
| 175 | if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; } |
| 176 | else if (obj.parent.webEmailsOverridePath != null) { emailsPath = obj.parent.webEmailsOverridePath; } |
| 177 | else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; } |
| 178 | if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null } |
| 179 | |
| 180 | // Get the non-english email if needed |
| 181 | var txtfile = null; |
| 182 | if ((lang != null) && (lang != 'en')) { |
| 183 | var translationsPath = obj.parent.path.join(emailsPath, 'translations'); |
| 184 | var translationsPathTxt = obj.parent.path.join(emailsPath, 'translations', 'sms-messages_' + lang + '.txt'); |
| 185 | if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) { |
| 186 | txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString(); |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | // Get the english email |
| 191 | if (txtfile == null) { |
| 192 | var pathTxt = obj.parent.path.join(emailsPath, 'sms-messages.txt'); |
| 193 | if (obj.parent.fs.existsSync(pathTxt)) { |
| 194 | txtfile = obj.parent.fs.readFileSync(pathTxt).toString(); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | // If no english sms and a non-english language is requested, try to get the default translated sms |
| 199 | if (txtfile == null && (lang != null) && (lang != 'en')) { |
| 200 | var translationsPath = obj.parent.path.join(obj.parent.webEmailsPath, 'translations'); |
| 201 | var translationsPathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', 'sms-messages_' + lang + '.txt'); |
| 202 | if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) { |
| 203 | txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString(); |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | // If no default translated sms, try to get the default english sms |
| 208 | if (txtfile == null) { |
| 209 | var pathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'sms-messages.txt'); |
| 210 | if (obj.parent.fs.existsSync(pathTxt)) { |
| 211 | txtfile = obj.parent.fs.readFileSync(pathTxt).toString(); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | // No email templates |
| 216 | if (txtfile == null) { return null; } |
| 217 | |
| 218 | // Decode the TXT file |
| 219 | var lines = txtfile.split('\r\n').join('\n').split('\n') |
| 220 | if (lines.length <= templateNumber) return null; |
| 221 | |
| 222 | return lines[templateNumber]; |
| 223 | } |
| 224 | |
| 225 | // Send phone number verification SMS |
| 226 | obj.sendPhoneCheck = function (domain, phoneNumber, verificationCode, language, func) { |
| 227 | parent.debug('email', "Sending verification SMS to " + phoneNumber); |
| 228 | |
| 229 | var sms = getTemplate(0, domain, language); |
| 230 | if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found |
| 231 | |
| 232 | // Setup the template |
| 233 | sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); |
| 234 | sms = sms.split('[[1]]').join(verificationCode); |
| 235 | |
| 236 | // Send the SMS |
| 237 | obj.sendSMS(phoneNumber, sms, func); |
| 238 | }; |
| 239 | |
| 240 | // Send phone number verification SMS |
| 241 | obj.sendToken = function (domain, phoneNumber, verificationCode, language, func) { |
| 242 | parent.debug('email', "Sending login token SMS to " + phoneNumber); |
| 243 | |
| 244 | var sms = getTemplate(1, domain, language); |
| 245 | if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found |
| 246 | |
| 247 | // Setup the template |
| 248 | sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); |
| 249 | sms = sms.split('[[1]]').join(verificationCode); |
| 250 | |
| 251 | // Send the SMS |
| 252 | obj.sendSMS(phoneNumber, sms, func); |
| 253 | }; |
| 254 | |
| 255 | return obj; |
| 256 | }; |