From 200acde9c875e945c501f6c0bab57a6326e84b85 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Wed, 22 Apr 2020 01:33:27 -0700 Subject: [PATCH] More work on SMS support. --- MeshCentralServer.njsproj | 1 + agents/meshcore.js | 7 +- emails/sms-messages.txt | 2 + emails/translations/sms-messages_cs.txt | 2 + emails/translations/sms-messages_de.txt | 2 + emails/translations/sms-messages_es.txt | 2 + emails/translations/sms-messages_fr.txt | 2 + emails/translations/sms-messages_hi.txt | 2 + emails/translations/sms-messages_ja.txt | 2 + emails/translations/sms-messages_ko.txt | 2 + emails/translations/sms-messages_nl.txt | 2 + emails/translations/sms-messages_pt.txt | 2 + emails/translations/sms-messages_ru.txt | 2 + emails/translations/sms-messages_zh-chs.txt | 2 + meshcentral.js | 12 +- meshmail.js | 10 +- meshsms.js | 120 ++++++++++++++++++++ meshuser.js | 63 ++++++++++ package.json | 1 + public/images/phone80.png | Bin 0 -> 3356 bytes translate/translate.js | 3 +- translate/translate.json | 18 ++- views/default.handlebars | 41 +++++++ webserver.js | 1 + 24 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 emails/sms-messages.txt create mode 100644 emails/translations/sms-messages_cs.txt create mode 100644 emails/translations/sms-messages_de.txt create mode 100644 emails/translations/sms-messages_es.txt create mode 100644 emails/translations/sms-messages_fr.txt create mode 100644 emails/translations/sms-messages_hi.txt create mode 100644 emails/translations/sms-messages_ja.txt create mode 100644 emails/translations/sms-messages_ko.txt create mode 100644 emails/translations/sms-messages_nl.txt create mode 100644 emails/translations/sms-messages_pt.txt create mode 100644 emails/translations/sms-messages_ru.txt create mode 100644 emails/translations/sms-messages_zh-chs.txt create mode 100644 meshsms.js create mode 100644 public/images/phone80.png diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 697f5c3d..566e0376 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -108,6 +108,7 @@ + diff --git a/agents/meshcore.js b/agents/meshcore.js index c56b0b67..833e874b 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -280,7 +280,7 @@ function createMeshCore(agent) { */ // MeshAgent JavaScript Core Module. This code is sent to and running on the mesh agent. - var meshCoreObj = { action: 'coreinfo', value: (require('MeshAgent').coreHash ? ('MeshCore CRC[' + crc32c(require('MeshAgent').coreHash) + ']') : ('MeshCore v6')), caps: 14 }; // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript, 32 = Temporary Agent, 64 = Recovery Agent + var meshCoreObj = { action: 'coreinfo', value: (require('MeshAgent').coreHash ? ('MeshCore CRC-' + crc32c(require('MeshAgent').coreHash)) : ('MeshCore v6')), caps: 14 }; // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript, 32 = Temporary Agent, 64 = Recovery Agent // Get the operating system description string @@ -1995,7 +1995,7 @@ function createMeshCore(agent) { var response = null; switch (cmd) { case 'help': { // Displays available commands - var fin = '', f = '', availcommands = 'startupoptions,alert,agentsize,version,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,nwslist,plugin,wsconnect,wssend,wsclose,notify,ls,ps,kill,amt,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,sendcaps,openurl,amtreset,amtccm,amtacm,amtdeactivate,amtpolicy,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,scanamt,wallpaper'; + var fin = '', f = '', availcommands = 'startupoptions,alert,agentsize,versions,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,nwslist,plugin,wsconnect,wssend,wsclose,notify,ls,ps,kill,amt,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,sendcaps,openurl,amtreset,amtccm,amtacm,amtdeactivate,amtpolicy,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,scanamt,wallpaper'; if (process.platform == 'win32') { availcommands += ',safemode,wpfhwacceleration'; } if (require('MeshAgent').maxKvmTileSize != null) { availcommands += ',kvmmode'; } @@ -2059,9 +2059,6 @@ function createMeshCore(agent) { } else { response = "Agent Size: " + actualSize + " kb"; } } else { response = "Agent Size: " + actualSize + " kb"; } break; - case 'version': - response = "Mesh Agent Version: " + process.versions.meshAgent; - break; case 'versions': response = JSON.stringify(process.versions, null, ' '); break; diff --git a/emails/sms-messages.txt b/emails/sms-messages.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/sms-messages.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_cs.txt b/emails/translations/sms-messages_cs.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_cs.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_de.txt b/emails/translations/sms-messages_de.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_de.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_es.txt b/emails/translations/sms-messages_es.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_es.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_fr.txt b/emails/translations/sms-messages_fr.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_fr.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_hi.txt b/emails/translations/sms-messages_hi.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_hi.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_ja.txt b/emails/translations/sms-messages_ja.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_ja.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_ko.txt b/emails/translations/sms-messages_ko.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_ko.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_nl.txt b/emails/translations/sms-messages_nl.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_nl.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_pt.txt b/emails/translations/sms-messages_pt.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_pt.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_ru.txt b/emails/translations/sms-messages_ru.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_ru.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/emails/translations/sms-messages_zh-chs.txt b/emails/translations/sms-messages_zh-chs.txt new file mode 100644 index 00000000..cffc6c48 --- /dev/null +++ b/emails/translations/sms-messages_zh-chs.txt @@ -0,0 +1,2 @@ +[[0]] verification code is: [[1]] +[[0]] access token is: [[1]] diff --git a/meshcentral.js b/meshcentral.js index 4f9b927c..a24b3ccf 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -30,6 +30,7 @@ function CreateMeshCentralServer(config, args) { obj.mqttbroker = null; obj.swarmserver = null; obj.mailserver = null; + obj.smsserver = null; obj.amtEventHandler = null; obj.pluginHandler = null; obj.amtScanner = null; @@ -1323,6 +1324,15 @@ function CreateMeshCentralServer(config, args) { if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode."); } } + // Setup SMS gateway + if (config.sms != null) { + obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj); + if (obj.smsserver != null) { + //obj.smsserver.verify(); + if (obj.args.lanonly == true) { addServerWarning("SMS gateway has limited use in LAN mode."); } + } + } + // Start periodic maintenance obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour @@ -2560,7 +2570,7 @@ function mainStart() { } // SMS support - if ((config.sms != null) && (config.sms.provider == 'twilio') && (typeof config.sms.sid == 'string') && (typeof config.sms.auth == 'string')) { modules.push('twilio'); } + if ((config.sms != null) && (config.sms.provider == 'twilio')) { modules.push('twilio'); } // Syslog support if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog'); } diff --git a/meshmail.js b/meshmail.js index 7227ec9e..87ae89f5 100644 --- a/meshmail.js +++ b/meshmail.js @@ -160,7 +160,7 @@ module.exports.CreateMeshMail = function (parent) { var template = getTemplate('account-login', domain, language); if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) { - parent.debug('email', "Error: Failed to get mail template."); // Not email template found + parent.debug('email', "Error: Failed to get mail template."); // No email template found return; } @@ -187,7 +187,7 @@ module.exports.CreateMeshMail = function (parent) { var template = getTemplate('account-invite', domain, language); if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) { - parent.debug('email', "Error: Failed to get mail template."); // Not email template found + parent.debug('email', "Error: Failed to get mail template."); // No email template found return; } @@ -214,7 +214,7 @@ module.exports.CreateMeshMail = function (parent) { var template = getTemplate('account-check', domain, language); if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) { - parent.debug('email', "Error: Failed to get mail template."); // Not email template found + parent.debug('email', "Error: Failed to get mail template."); // No email template found return; } @@ -242,7 +242,7 @@ module.exports.CreateMeshMail = function (parent) { var template = getTemplate('account-reset', domain, language); if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) { - parent.debug('email', "Error: Failed to get mail template."); // Not email template found + parent.debug('email', "Error: Failed to get mail template."); // No email template found return; } @@ -270,7 +270,7 @@ module.exports.CreateMeshMail = function (parent) { var template = getTemplate('mesh-invite', domain, language); if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) { - parent.debug('email', "Error: Failed to get mail template."); // Not email template found + parent.debug('email', "Error: Failed to get mail template."); // No email template found return; } diff --git a/meshsms.js b/meshsms.js new file mode 100644 index 00000000..fecb209e --- /dev/null +++ b/meshsms.js @@ -0,0 +1,120 @@ +/** +* @description MeshCentral SMS gateway communication module +* @author Ylian Saint-Hilaire +* @copyright Intel Corporation 2018-2020 +* @license Apache-2.0 +* @version v0.0.1 +*/ + +/*xjslint node: true */ +/*xjslint plusplus: true */ +/*xjslint maxlen: 256 */ +/*jshint node: true */ +/*jshint strict: false */ +/*jshint esversion: 6 */ +"use strict"; + +// Construct a MeshAgent object, called upon connection +module.exports.CreateMeshSMS = function (parent) { + var obj = {}; + obj.parent = parent; + obj.provider = null; + + // SMS gateway provider setup + switch (parent.config.sms.provider) { + case 'twilio': { + // Validate Twilio configuration values + if (typeof parent.config.sms.sid != 'string') { console.log('Invalid or missing SMS gateway provider sid.'); return null; } + if (typeof parent.config.sms.auth != 'string') { console.log('Invalid or missing SMS gateway provider auth.'); return null; } + if (typeof parent.config.sms.from != 'string') { console.log('Invalid or missing SMS gateway provider from.'); return null; } + + // Setup Twilio + var Twilio = require('twilio'); + obj.provider = new Twilio(parent.config.sms.sid, parent.config.sms.auth); + break; + } + default: { + // Unknown SMS gateway provider + console.log('Unknown SMS gateway provider: ' + parent.config.sms.provider); + return null; + } + } + + // Send an SMS message + obj.sendSMS = function (to, msg, func) { + parent.debug('email', 'Sending SMS to: ' + to + ': ' + msg); + console.log({ from: parent.config.sms.from, to: to, body: msg }); + if (parent.config.sms.provider == 'twilio') { + obj.provider.messages.create({ + from: parent.config.sms.from, + to: to, + body: msg + }, function (err, result) { + if (err != null) { parent.debug('email', 'SMS error: ' + JSON.stringify(err)); } else { parent.debug('email', 'SMS result: ' + JSON.stringify(result)); } + if (func != null) { func((err == null) && (result.status == 'queued'), err, result); } + }); + } + } + + // Get the correct SMS template + function getTemplate(templateNumber, domain, lang) { + parent.debug('email', 'Getting SMS template #' + templateNumber + ', lang: ' + lang); + if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given. + + var r = {}, emailsPath = null; + if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; } + else if (obj.parent.webEmailsOverridePath != null) { emailsPath = obj.parent.webEmailsOverridePath; } + else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; } + if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null } + + // Get the non-english email if needed + var txtfile = null; + if ((lang != null) && (lang != 'en')) { + var translationsPath = obj.parent.path.join(emailsPath, 'translations'); + var translationsPathTxt = obj.parent.path.join(emailsPath, 'translations', 'sms-messages_' + lang + '.txt'); + if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) { + txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString(); + } + } + + // Get the english email + if ((htmlfile == null) || (txtfile == null)) { + var pathTxt = obj.parent.path.join(emailsPath, 'sms-messages.txt'); + if (obj.parent.fs.existsSync(pathTxt)) { + txtfile = obj.parent.fs.readFileSync(pathTxt).toString(); + } + } + + // No email templates + if (txtfile == null) { return null; } + + // Decode the TXT file + lines = txtfile.split('\r\n').join('\n').split('\n') + if (lines.length >= templateNumber) return null; + + return lines[templateNumber]; + } + + // Send phone number verification SMS + obj.sendPhoneCheck = function (domain, phoneNumber, verificationCode, language, func) { + parent.debug('email', "Sending verification SMS to " + phoneNumber); + + var template = getTemplate(0, domain, language); + if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) { + parent.debug('email', "Error: Failed to get SMS template"); // No SMS template found + return; + } + + // Setup the template + template.split("[[0]]").join(domain.title ? domain.title : 'MeshCentral'); + template.split("[[1]]").join(verificationCode); + + // Send the SMS + obj.sendSMS(phoneNumber, template, func); + }; + + return obj; +}; + +// +18632703894 +// SMS 5032700426 "This is a test" diff --git a/meshuser.js b/meshuser.js index 41b89a40..0507a711 100644 --- a/meshuser.js +++ b/meshuser.js @@ -755,6 +755,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'sms': { + if (parent.parent.smsserver == null) { + r = "No SMS gateway in use."; + } else { + if (cmdargs['_'].length != 2) { + r = "Usage: SMS \"PhoneNumber\" \"Message\"."; + } else { + parent.parent.smsserver.sendSMS(cmdargs['_'][0], cmdargs['_'][1], function (status) { + try { ws.send(JSON.stringify({ action: 'serverconsole', value: status?'Success':'Failed', tag: command.tag })); } catch (ex) { } + }); + } + } + break; + } case 'le': { if (parent.parent.letsencrypt == null) { r = "Let's Encrypt not in use."; @@ -3695,6 +3709,55 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use delete obj.hardwareKeyRegistrationRequest; break; } + case 'verifyPhone': { + if (parent.parent.smsserver == null) return; + if (common.validateString(command.phone, 1, 18) == false) break; // Check phone length + if (command.phone.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) == false) break; // Check phone + var code = getRandomEightDigitInteger(); + //console.log(code); + + // TODO: We need to tie this cookie to this session and limit how many times we can guess the code + const phoneCookie = parent.parent.encodeCookie({ a: 'verifyPhone', c: code, p: command.phone }); + + ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: true })); // DEBUG + /* + parent.parent.smsserver.sendPhoneCheck(domain, command.phone, code, parent.getLanguageCodes(req), function (success) { + ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: success })); + }); + */ + break; + } + case 'confirmPhone': { + if ((parent.parent.smsserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'number')) break; // Input checks + var cookie = parent.parent.decodeCookie(command.cookie); + if (cookie == null) break; // Invalid cookie + if (cookie.c != command.code) { ws.send(JSON.stringify({ action: 'verifyPhone', cookie: command.cookie, success: true })); break; } // Code does not match + + // Set the user's phone + user.phone = cookie.p; + db.SetUser(user); + + // Event the change + var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msg: 'Verified phone number of user ' + EscapeHtml(user.name), domain: domain.id }; + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come. + parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event); + + break; + } + case 'removePhone': { + if (user.phone == null) break; + + // Clear the user's phone + delete user.phone; + db.SetUser(user); + + // Event the change + var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msg: 'Removed phone number of user ' + EscapeHtml(user.name), domain: domain.id }; + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come. + parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event); + + break; + } case 'getClip': { if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid diff --git a/package.json b/package.json index 2779fc5d..a86397f3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ ], "dependencies": { "archiver": "^3.0.0", + "archiver-zip-encrypted": "^1.0.8", "body-parser": "^1.19.0", "cbor": "^4.1.5", "compression": "^1.7.4", diff --git a/public/images/phone80.png b/public/images/phone80.png new file mode 100644 index 0000000000000000000000000000000000000000..4aa55046fcb8cf92901b394476aed25bfa354519 GIT binary patch literal 3356 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 mJv~e0-cq5d8container->page_content->column_l->1->1->0->1->checkemailpanel->1->checkCheckOperations->1->2->1->1", "login.handlebars->container->column_l->centralTable->1->0->logincell->checkemailpanel->1->checkCheckOperations->1->2->1->1" @@ -9004,6 +9003,9 @@ "default.handlebars->29->889" ] }, + { + "en": "E-mailadres wijzigen" + }, { "cs": "CHYBA: ", "de": "FEHLER:", @@ -27935,6 +27937,18 @@ "default.handlebars->29->1039" ] }, + { + "en": "[[0]] access token is: [[1]]", + "xloc": [ + "sms-messages.txt" + ] + }, + { + "en": "[[0]] verification code is: [[1]]", + "xloc": [ + "sms-messages.txt" + ] + }, { "cs": "[[[SERVERNAME]]]", "de": "[[[SERVERNAME]]]", @@ -29682,4 +29696,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index 494b65eb..bf3aecec 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -306,6 +306,7 @@