First pass at adding Telegram support (#4650)

This commit is contained in:
Ylian Saint-Hilaire 2022-10-22 07:23:55 -07:00
parent 9c0f44fc96
commit 7e3dce0ef7
8 changed files with 394 additions and 25 deletions

View File

@ -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" ]

View File

@ -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'); }

151
meshmessaging.js Normal file
View File

@ -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;
};

View File

@ -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;

View File

@ -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.";

View File

@ -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"
}
}
}

136
telegram.js Normal file
View File

@ -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;
};

View File

@ -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 };
}