From 7e3dce0ef7c02cb213e5842992d7eb081e50f101 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 22 Oct 2022 07:23:55 -0700 Subject: [PATCH] First pass at adding Telegram support (#4650) --- meshcentral-config-schema.json | 51 ++++++++--- meshcentral.js | 39 ++++++--- meshmessaging.js | 151 +++++++++++++++++++++++++++++++++ meshsms.js | 8 +- meshuser.js | 25 +++++- sample-config-advanced.json | 7 ++ telegram.js | 136 +++++++++++++++++++++++++++++ webserver.js | 2 + 8 files changed, 394 insertions(+), 25 deletions(-) create mode 100644 meshmessaging.js create mode 100644 telegram.js diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index c35ac0ac..e7c9dc06 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -1299,13 +1299,16 @@ "required": [ "host", "port", "from", "tls" ] }, "sms": { - "title" : "SMS provider", + "title": "SMS provider", "description": "Connects MeshCentral to a SMS text messaging provider, allows MeshCentral to send SMS messages for 2FA or user notification.", "oneOf": [ { - "type": "object", + "type": "object", "properties": { - "provider": { "type": "string", "enum": [ "twilio" ] }, + "provider": { + "type": "string", + "enum": [ "twilio" ] + }, "sid": { "type": "string" }, "auth": { "type": "string" }, "from": { "type": "string" } @@ -1313,9 +1316,12 @@ "required": [ "provider", "sid", "auth", "from" ] }, { - "type": "object", + "type": "object", "properties": { - "provider": { "type": "string", "enum": [ "plivo" ] }, + "provider": { + "type": "string", + "enum": [ "plivo" ] + }, "id": { "type": "string" }, "token": { "type": "string" }, "from": { "type": "string" } @@ -1323,23 +1329,48 @@ "required": [ "provider", "id", "token", "from" ] }, { - "type": "object", + "type": "object", "properties": { - "provider": { "type": "string", "enum": [ "telnyx" ] }, + "provider": { + "type": "string", + "enum": [ "telnyx" ] + }, "apikey": { "type": "string" }, "from": { "type": "string" } }, "required": [ "provider", "apikey", "from" ] }, { - "type": "object", + "type": "object", "properties": { - "provider": { "type": "string", "enum": [ "url" ] }, - "url": { "type": "string", "description": "A http or https URL with {{phone}} and {{message}} in the string. These will be replaced with the URL encoded target phone number and message." } + "provider": { + "type": "string", + "enum": [ "url" ] + }, + "url": { + "type": "string", + "description": "A http or https URL with {{phone}} and {{message}} in the string. These will be replaced with the URL encoded target phone number and message." + } }, "required": [ "url" ] } ] + }, + "messaging": { + "title" : "Messaging server", + "description": "This section allow MeshCentral to send messages over user messaging networks like Telegram", + "type": "object", + "properties": { + "telegram": { + "type": "object", + "description": "Configure Telegram messaging system", + "properties": { + "apiid": { "type": "number" }, + "apihash": { "type": "string" }, + "session": { "type": "string" } + } + } + } } }, "required": [ "settings", "domains" ] diff --git a/meshcentral.js b/meshcentral.js index 11f181e4..b4bf7d9c 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -22,19 +22,20 @@ if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ function CreateMeshCentralServer(config, args) { const obj = {}; obj.db = null; - obj.webserver = null; - obj.redirserver = null; - obj.mpsserver = null; - obj.mqttbroker = null; - obj.swarmserver = null; - obj.smsserver = null; + obj.webserver = null; // HTTPS main web server, typically on port 443 + obj.redirserver = null; // HTTP relay web server, typically on port 80 + obj.mpsserver = null; // Intel AMT CIRA server, typically on port 4433 + obj.mqttbroker = null; // MQTT server, not is not often used + obj.swarmserver = null; // Swarm server, this is used only to update older MeshCentral v1 agents + obj.smsserver = null; // SMS server, used to send user SMS messages + obj.msgserver = null; // Messaging server, used to sent used messages obj.amtEventHandler = null; obj.pluginHandler = null; obj.amtScanner = null; - obj.amtManager = null; + obj.amtManager = null; // Intel AMT manager, used to oversee all Intel AMT devices, activate them and sync policies obj.meshScanner = null; obj.taskManager = null; - obj.letsencrypt = null; + obj.letsencrypt = null; // Let's encrypt server, used to get and renew TLS certificates obj.eventsDispatch = {}; obj.fs = require('fs'); obj.path = require('path'); @@ -758,7 +759,7 @@ function CreateMeshCentralServer(config, args) { } // Check top level configuration for any unrecognized values - if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [ i ]); } } } + if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [ i ]); } } } // Read IP lists from files if applicable config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip); @@ -858,7 +859,7 @@ function CreateMeshCentralServer(config, args) { if (err != null) { console.log("Database error: " + err); process.exit(); return; } if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; } const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account. - delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; // Disable 2FA + delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; // Disable 2FA if (obj.args.hashpass) { // Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password. var hashpasssplit = obj.args.hashpass.split(','); @@ -1777,6 +1778,11 @@ function CreateMeshCentralServer(config, args) { if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); } } + // Setup user messaging + if (config.messaging != null) { + obj.msgserver = require('./meshmessaging.js').CreateServer(obj); + } + // Setup web based push notifications if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { obj.webpush = require('web-push'); @@ -4008,9 +4014,16 @@ function mainStart() { if (config.settings.desktopmultiplex === true) { modules.push('image-size'); } // SMS support - if ((config.sms != null) && (config.sms.provider == 'twilio')) { modules.push('twilio'); } - if ((config.sms != null) && (config.sms.provider == 'plivo')) { modules.push('plivo'); } - if ((config.sms != null) && (config.sms.provider == 'telnyx')) { modules.push('telnyx'); } + if (config.sms != null) { + if (config.sms.provider == 'twilio') { modules.push('twilio'); } + if (config.sms.provider == 'plivo') { modules.push('plivo'); } + if (config.sms.provider == 'telnyx') { modules.push('telnyx'); } + } + + // Messaging support + if (config.messaging != null) { + if (config.messaging.telegram != null) { modules.push('telegram'); modules.push('input'); } + } // Setup web based push notifications if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push'); } diff --git a/meshmessaging.js b/meshmessaging.js new file mode 100644 index 00000000..3c7a8f02 --- /dev/null +++ b/meshmessaging.js @@ -0,0 +1,151 @@ +/** +* @description MeshCentral user messaging communication module +* @author Ylian Saint-Hilaire +* @copyright Intel Corporation 2022 +* @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"; + +/* +// For Telegram, add this in config.json +"messaging": { + "telegram": { + "apiid": 00000000, + "apihash": "00000000000000000000000", + "session": "aaaaaaaaaaaaaaaaaaaaaaa" + } +} +*/ + +// Construct a SMS server object +module.exports.CreateServer = function (parent) { + var obj = {}; + obj.parent = parent; + obj.providers = 0; // 1 = Telegram, 2 = Signal + obj.telegramClient = null; + + // Messaging client setup + if (parent.config.messaging.telegram) { + // Validate Telegram configuration values + var telegramOK = true; + if (typeof parent.config.messaging.telegram.apiid != 'number') { console.log('Invalid or missing Telegram apiid.'); telegramOK = false; } + if (typeof parent.config.messaging.telegram.apihash != 'string') { console.log('Invalid or missing Telegram apihash.'); telegramOK = false; } + if (typeof parent.config.messaging.telegram.session != 'string') { console.log('Invalid or missing Telegram session.'); telegramOK = false; } + + if (telegramOK) { + // Setup Telegram + async function setupTelegram() { + const { TelegramClient } = require('telegram'); + const { StringSession } = require('telegram/sessions'); + const input = require('input'); + const stringSession = new StringSession(parent.config.messaging.telegram.session); + const client = new TelegramClient(stringSession, parent.config.messaging.telegram.apiid, parent.config.messaging.telegram.apihash, { connectionRetries: 5 }); + await client.start({ + phoneNumber: async function () { await input.text("Please enter your number: "); }, + password: async function () { await input.text("Please enter your password: "); }, + phoneCode: async function () { await input.text("Please enter the code you received: "); }, + onError: function (err) { console.log('Telegram error', err); }, + }); + obj.telegramClient = client; + obj.providers += 1; // Enable Telegram messaging + console.log("MeshCentral Telegram client is connected."); + } + setupTelegram(); + } + } + + // Send an user message + obj.sendMessage = function(to, msg, func) { + // Telegram + if ((to.startsWith('telegram:')) && (obj.telegramClient != null)) { + async function sendTelegramMessage(to, msg, func) { + if (obj.telegramClient == null) return; + parent.debug('email', 'Sending Telegram message to: ' + to.substring(9) + ': ' + msg); + try { await obj.telegramClient.sendMessage(to.substring(9), { message: msg }); func(true); } catch (ex) { func(false, ex); } + } + sendTelegramMessage(to, msg, func); + } else { + // No providers found + func(false, "No messaging providers found for this message."); + } + } + + // 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 (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 + var 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, to, verificationCode, language, func) { + parent.debug('email', "Sending verification message to " + to); + + var sms = getTemplate(0, domain, language); + if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found + + // Setup the template + sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); + sms = sms.split('[[1]]').join(verificationCode); + + // Send the SMS + obj.sendSMS(to, sms, func); + }; + + // Send phone number verification SMS + obj.sendToken = function (domain, to, verificationCode, language, func) { + parent.debug('email', "Sending login token message to " + to); + + var sms = getTemplate(1, domain, language); + if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found + + // Setup the template + sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); + sms = sms.split('[[1]]').join(verificationCode); + + // Send the SMS + obj.sendSMS(to, sms, func); + }; + + return obj; +}; diff --git a/meshsms.js b/meshsms.js index 25630000..1746c23e 100644 --- a/meshsms.js +++ b/meshsms.js @@ -37,9 +37,15 @@ "apikey": "xxxxxxx", "from": "15555555555" } + +// For URL, add this in config.json +"sms": { + "provider": "url", + "url": "https://sample.com/?phone={{phone}}&msg={{message}}" +} */ -// Construct a MeshAgent object, called upon connection +// Construct a SMS server object module.exports.CreateMeshSMS = function (parent) { var obj = {}; obj.parent = parent; diff --git a/meshuser.js b/meshuser.js index 5e3e68bd..80c3fd6d 100644 --- a/meshuser.js +++ b/meshuser.js @@ -575,6 +575,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (domain.passwordrequirements.lock2factor == true) { serverinfo.lock2factor = true; } // Indicate 2FA change are not allowed if (typeof domain.passwordrequirements.maxfidokeys == 'number') { serverinfo.maxfidokeys = domain.passwordrequirements.maxfidokeys; } } + if (parent.parent.msgserver != null) { serverinfo.userMsgProviders = parent.parent.msgserver.providers; } // Build the mobile agent URL, this is used to connect mobile devices var agentServerName = parent.getWebServerName(domain, req); @@ -5315,7 +5316,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'serverupdate': [serverUserCommandServerUpdate, "Updates server to latest version. Optional version argument to install specific version. Example: serverupdate 0.8.49"], 'setmaxtasks': [serverUserCommandSetMaxTasks, ""], 'showpaths': [serverUserCommandShowPaths, ""], - 'sms': [serverUserCommandSMS, ""], + 'sms': [serverUserCommandSMS, "Send a SMS message to a specified phone number"], + 'msg': [serverUserCommandMsg, "Send a user message to a user handle"], 'swarmstats': [serverUserCommandSwarmStats, ""], 'tasklimiter': [serverUserCommandTaskLimiter, "Returns the internal status of the tasklimiter. This is a system used to smooth out work done by the server. It's used by, for example, agent updates so that not all agents are updated at the same time."], 'trafficdelta': [serverUserCommandTrafficDelta, ""], @@ -6727,6 +6729,27 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } } + function serverUserCommandMsg(cmdData) { + if ((parent.parent.msgserver == null) || (parent.parent.msgserver.providers == 0)) { + cmdData.result = "No messaging providers configured."; + } else { + if (cmdData.cmdargs['_'].length != 2) { + var r = []; + if ((parent.parent.msgserver.providers & 1) != 0) { r.push("Usage: MSG \"telegram:@UserHandle\" \"Message\"."); } + if ((parent.parent.msgserver.providers & 2) != 0) { r.push("Usage: MSG \"signal:@UserHandle\" \"Message\"."); } + cmdData.result = r.join('\r\n'); + } else { + parent.parent.msgserver.sendMessage(cmdData.cmdargs['_'][0], cmdData.cmdargs['_'][1], function (status, msg) { + if (typeof msg == 'string') { + try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? ('Success: ' + msg) : ('Failed: ' + msg), tag: cmdData.command.tag })); } catch (ex) { } + } else { + try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? 'Success' : 'Failed', tag: cmdData.command.tag })); } catch (ex) { } + } + }); + } + } + } + function serverUserCommandEmail(cmdData) { if (domain.mailserver == null) { cmdData.result = "No email service enabled."; diff --git a/sample-config-advanced.json b/sample-config-advanced.json index c8a26f30..234eae64 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -614,5 +614,12 @@ "____sms": { "provider": "url", "url": "http://example.com/sms.ashx?phone={{phone}}&message={{message}}" + }, + "_messaging": { + "telegram": { + "apiid": 0, + "apihash": "hexBalue", + "session": "base64Value" + } } } diff --git a/telegram.js b/telegram.js new file mode 100644 index 00000000..7c7478a8 --- /dev/null +++ b/telegram.js @@ -0,0 +1,136 @@ +/** +* @description MeshCentral Telegram communication module +* @author Ylian Saint-Hilaire +* @copyright Intel Corporation 2018-2022 +* @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"; + +/* + "telegram": { + "apiid": 0000000, + "apihash": "hexvalue", + "session": "base64value" + } +*/ + +// Construct a Telegram server object +module.exports.CreateServer = function (parent) { + var obj = {}; + obj.parent = parent; + + // Check that we have the correct values + if (typeof parent.config.telegram != 'object') return null; + if (typeof parent.config.telegram.apiid != 'number') return null; + if (typeof parent.config.telegram.apihash != 'string') return null; + if (typeof parent.config.telegram.session != 'string') return null; + + // Connect to the telegram server + async function connect() { + const { TelegramClient } = require('telegram'); + const { StringSession } = require('telegram/sessions'); + const input = require('input'); + + const stringSession = new StringSession(parent.config.telegram.session); + const client = new TelegramClient(stringSession, parent.config.telegram.apiid, parent.config.telegram.apihash, { connectionRetries: 5 }); + await client.start({ + phoneNumber: async function () { await input.text("Please enter your number: "); }, + password: async function () { await input.text("Please enter your password: "); }, + phoneCode: async function () { await input.text("Please enter the code you received: "); }, + onError: function (err) { console.log('Telegram error', err); }, + }); + console.log("MeshCentral Telegram session is connected."); + obj.client = client; + //console.log(client.session.save()); // Save this string to avoid logging in again + } + + // Send an Telegram message + obj.sendMessage = async function (to, msg, func) { + if (obj.client == null) return; + parent.debug('email', 'Sending Telegram to: ' + to + ': ' + msg); + await client.sendMessage(to, { message: msg }); + func(true); + } + + // 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 (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 + var lines = txtfile.split('\r\n').join('\n').split('\n') + if (lines.length <= templateNumber) return null; + + return lines[templateNumber]; + } + + // Send telegram user verification message + obj.sendPhoneCheck = function (domain, to, verificationCode, language, func) { + parent.debug('email', "Sending verification Telegram to " + to); + + var sms = getTemplate(0, domain, language); + if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found + + // Setup the template + sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); + sms = sms.split('[[1]]').join(verificationCode); + + // Send the SMS + obj.sendMessage(to, sms, func); + }; + + // Send login token verification message + obj.sendToken = function (domain, to, verificationCode, language, func) { + parent.debug('email', "Sending login token Telegram to " + to); + + var sms = getTemplate(1, domain, language); + if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found + + // Setup the template + sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); + sms = sms.split('[[1]]').join(verificationCode); + + // Send the SMS + obj.sendMessage(to, sms, func); + }; + + // Connect the Telegram session + connect(); + + return obj; +}; diff --git a/webserver.js b/webserver.js index 0380a2b4..928ecd7b 100644 --- a/webserver.js +++ b/webserver.js @@ -3162,6 +3162,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server if ((typeof domain.files == 'object') && (domain.files.sftpconnect === false)) { features2 += 0x00800000; } // Remove the "SFTP Connect" button in the "Files" tab when the device is agent managed if ((typeof domain.terminal == 'object') && (domain.terminal.sshconnect === false)) { features2 += 0x01000000; } // Remove the "SSH Connect" button in the "Terminal" tab when the device is agent managed + if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled + if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed return { features: features, features2: features2 }; }