/**
* @description MeshCentral Common Library
* @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';
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const { URL } = require('url');
// Binary encoding and decoding functions
module.exports.ReadShort = function (v, p) { return (v.charCodeAt(p) << 8) + v.charCodeAt(p + 1); };
module.exports.ReadShortX = function (v, p) { return (v.charCodeAt(p + 1) << 8) + v.charCodeAt(p); };
module.exports.ReadInt = function (v, p) { return (v.charCodeAt(p) * 0x1000000) + (v.charCodeAt(p + 1) << 16) + (v.charCodeAt(p + 2) << 8) + v.charCodeAt(p + 3); }; // We use "*0x1000000" instead of "<<24" because the shift converts the number to signed int32.
module.exports.ReadIntX = function (v, p) { return (v.charCodeAt(p + 3) * 0x1000000) + (v.charCodeAt(p + 2) << 16) + (v.charCodeAt(p + 1) << 8) + v.charCodeAt(p); };
module.exports.ShortToStr = function (v) { return String.fromCharCode((v >> 8) & 0xFF, v & 0xFF); };
module.exports.ShortToStrX = function (v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF); };
module.exports.IntToStr = function (v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); };
module.exports.IntToStrX = function (v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF); };
module.exports.MakeToArray = function (v) { if (!v || v == null || typeof v == 'object') return v; return [v]; };
module.exports.SplitArray = function (v) { return v.split(','); };
module.exports.Clone = function (v) { return JSON.parse(JSON.stringify(v)); };
module.exports.StringifyRedacted = function (v) { return JSON.stringify(v, function (k, x) { return ((x != null) && /secret|password|passwd|private|pvk|token$/i.test(k)) ? '****' : x; }); }; // For logs, keeps keys but masks credential values
module.exports.IsFilenameValid = (function () { var x1 = /^[^\\/:\*\?"<>\|]+$/, x2 = /^\./, x3 = /^(nul|prn|con|lpt[0-9]|com[0-9])(\.|$)/i; return function isFilenameValid(fname) { return module.exports.validateString(fname, 1, 4096) && x1.test(fname) && !x2.test(fname) && !x3.test(fname) && (fname[0] != '.'); }; })();
module.exports.makeFilename = function (v) { return v.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join(''); }
module.exports.joinPath = function (base, path_) { return path.isAbsolute(path_) ? path_ : path.join(base, path_); }
// Move an element from one position in an array to a new position
module.exports.ArrayElementMove = function(arr, from, to) { arr.splice(to, 0, arr.splice(from, 1)[0]); };
// Format a string with arguments, "replaces {0} and {1}..."
module.exports.format = function (format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
// Print object for HTML
module.exports.ObjectToStringEx = function (x, c) {
var r = '', i;
if (x != 0 && (!x || x == null)) return "(Null)";
if (x instanceof Array) { for (i in x) { r += '
' + gap(c) + "Item #" + i + ": " + module.exports.ObjectToStringEx(x[i], c + 1); } }
else if (x instanceof Object) { for (i in x) { r += '
' + gap(c) + i + " = " + module.exports.ObjectToStringEx(x[i], c + 1); } }
else { r += x; }
return r;
};
// Print object for console
module.exports.ObjectToStringEx2 = function (x, c) {
var r = '', i;
if (x != 0 && (!x || x == null)) return "(Null)";
if (x instanceof Array) { for (i in x) { r += '\r\n' + gap2(c) + "Item #" + i + ": " + module.exports.ObjectToStringEx2(x[i], c + 1); } }
else if (x instanceof Object) { for (i in x) { r += '\r\n' + gap2(c) + i + " = " + module.exports.ObjectToStringEx2(x[i], c + 1); } }
else { r += x; }
return r;
};
// Create an ident gap
module.exports.gap = function (c) { var x = ''; for (var i = 0; i < (c * 4); i++) { x += ' '; } return x; };
module.exports.gap2 = function (c) { var x = ''; for (var i = 0; i < (c * 4); i++) { x += ' '; } return x; };
// Print an object in html
module.exports.ObjectToString = function (x) { return module.exports.ObjectToStringEx(x, 0); };
module.exports.ObjectToString2 = function (x) { return module.exports.ObjectToStringEx2(x, 0); };
// Convert a hex string to a raw string
module.exports.hex2rstr = function (d) {
var r = '', m = ('' + d).match(/../g), t;
while (t = m.shift()) { r += String.fromCharCode('0x' + t); }
return r;
};
// Convert decimal to hex
module.exports.char2hex = function (i) { return (i + 0x100).toString(16).substr(-2).toUpperCase(); };
// Convert a raw string to a hex string
module.exports.rstr2hex = function (input) {
var r = '', i;
for (i = 0; i < input.length; i++) { r += module.exports.char2hex(input.charCodeAt(i)); }
return r;
};
// UTF-8 encoding & decoding functions
module.exports.encode_utf8 = function (s) { return unescape(encodeURIComponent(s)); };
module.exports.decode_utf8 = function (s) { return decodeURIComponent(escape(s)); };
// Convert a string into a blob
module.exports.data2blob = function (data) {
var bytes = new Array(data.length);
for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
var blob = new Blob([new Uint8Array(bytes)]);
return blob;
};
// Generate random numbers between 0 and max without bias.
module.exports.random = function (max) {
const crypto = require('crypto');
var maxmask = 1, r;
while (maxmask < max) { maxmask = (maxmask << 1) + 1; }
do { r = (crypto.randomBytes(4).readUInt32BE(0) & maxmask); } while (r > max);
return r;
};
// Split a comma separated string, ignoring commas in quotes.
module.exports.quoteSplit = function (str) {
var tmp = '', quote = 0, result = [];
for (var i in str) { if (str[i] == '"') { quote = (quote + 1) % 2; } if ((str[i] == ',') && (quote == 0)) { tmp = tmp.trim(); result.push(tmp); tmp = ''; } else { tmp += str[i]; } }
if (tmp.length > 0) result.push(tmp.trim());
return result;
};
// Convert list of "name = value" into object
module.exports.parseNameValueList = function (list) {
var result = [];
for (var i in list) {
var j = list[i].indexOf('=');
if (j > 0) {
var v = list[i].substring(j + 1).trim();
if ((v[0] == '"') && (v[v.length - 1] == '"')) { v = v.substring(1, v.length - 1); }
result[list[i].substring(0, j).trim()] = v;
}
}
return result;
};
// Compute the MD5 digest hash for a set of values
module.exports.ComputeDigesthash = function (username, password, realm, method, path, qop, nonce, nc, cnonce) {
var ha1 = crypto.createHash('md5').update(username + ":" + realm + ":" + password).digest('hex');
var ha2 = crypto.createHash('md5').update(method + ":" + path).digest('hex');
return crypto.createHash('md5').update(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2).digest("hex");
};
module.exports.toNumber = function (str) { var x = parseInt(str); if (x == str) return x; return str; };
module.exports.escapeHtml = function (string) { return String(string).replace(/[&<>"'`=\/]/g, function (s) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=' }[s]; }); };
module.exports.escapeHtmlBreaks = function (string) { return String(string).replace(/[&<>"'`=\/]/g, function (s) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', '\r': '
', '\n': '' }[s]; }); };
module.exports.zeroPad = function(num, c) { if (c == null) { c = 2; } var s = '000000' + num; return s.substr(s.length - c); }
// Lowercase all the names in a object recursively
// Allow for exception keys, child of exceptions will not get lower-cased.
// Exceptions is an array of "keyname" or "parent\keyname"
module.exports.objKeysToLower = function (obj, exceptions, parent) {
for (var i in obj) {
if ((typeof obj[i] == 'object') &&
((exceptions == null) || (exceptions.indexOf(i.toLowerCase()) == -1) && ((parent == null) || (exceptions.indexOf(parent.toLowerCase() + '/' + i.toLowerCase()) == -1)))
) {
module.exports.objKeysToLower(obj[i], exceptions, i); // LowerCase all key names in the child object
}
if (i.toLowerCase() !== i) { obj[i.toLowerCase()] = obj[i]; delete obj[i]; } // LowerCase all key names
}
return obj;
};
// Escape and unescape field names so there are no invalid characters for MongoDB/NeDB ("$", ",", ".", see https://github.com/seald/nedb/tree/master?tab=readme-ov-file#inserting-documents)
module.exports.escapeFieldName = function (name) { if ((name.indexOf(',') == -1) && (name.indexOf('%') == -1) && (name.indexOf('.') == -1) && (name.indexOf('$') == -1)) return name; return name.split('%').join('%25').split('.').join('%2E').split('$').join('%24').split(',').join('%2C'); };
module.exports.unEscapeFieldName = function (name) { if (name.indexOf('%') == -1) return name; return name.split('%2C').join(',').split('%2E').join('.').split('%24').join('$').split('%25').join('%'); };
// Escape all links, SSH and RDP usernames
// This is required for databases like NeDB that don't accept "." or "," as part of a field name.
module.exports.escapeLinksFieldNameEx = function (docx) { if ((docx.links == null) && (docx.ssh == null) && (docx.rdp == null)) { return docx; } return module.exports.escapeLinksFieldName(docx); };
module.exports.escapeLinksFieldName = function (docx) {
var doc = Object.assign({}, docx);
if (doc.links != null) { doc.links = Object.assign({}, doc.links); for (var i in doc.links) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.links[ue] = doc.links[i]; delete doc.links[i]; } } }
if (doc.ssh != null) { doc.ssh = Object.assign({}, doc.ssh); for (var i in doc.ssh) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.ssh[ue] = doc.ssh[i]; delete doc.ssh[i]; } } }
if (doc.rdp != null) { doc.rdp = Object.assign({}, doc.rdp); for (var i in doc.rdp) { var ue = module.exports.escapeFieldName(i); if (ue !== i) { doc.rdp[ue] = doc.rdp[i]; delete doc.rdp[i]; } } }
return doc;
};
module.exports.unEscapeLinksFieldName = function (doc) {
if (doc.links != null) { for (var j in doc.links) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.links[ue] = doc.links[j]; delete doc.links[j]; } } }
if (doc.ssh != null) { for (var j in doc.ssh) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.ssh[ue] = doc.ssh[j]; delete doc.ssh[j]; } } }
if (doc.rdp != null) { for (var j in doc.rdp) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.rdp[ue] = doc.rdp[j]; delete doc.rdp[j]; } } }
return doc;
};
//module.exports.escapeAllLinksFieldName = function (docs) { for (var i in docs) { module.exports.escapeLinksFieldName(docs[i]); } return docs; };
module.exports.unEscapeAllLinksFieldName = function (docs) { for (var i in docs) { docs[i] = module.exports.unEscapeLinksFieldName(docs[i]); } return docs; };
// Escape field names for aceBase
var aceEscFields = ['links', 'ssh', 'rdp', 'notify'];
module.exports.aceEscapeFieldNames = function (docx) { var doc = Object.assign({}, docx); for (var k in aceEscFields) { if (typeof doc[aceEscFields[k]] == 'object') { doc[aceEscFields[k]] = Object.assign({}, doc[aceEscFields[k]]); for (var i in doc[aceEscFields[k]]) { var ue = encodeURIComponent(i); if (ue !== i) { doc[aceEscFields[k]][ue] = doc[aceEscFields[k]][i]; delete doc[aceEscFields[k]][i]; } } } } return doc; };
module.exports.aceUnEscapeFieldNames = function (doc) { for (var k in aceEscFields) { if (typeof doc[aceEscFields[k]] == 'object') { for (var j in doc[aceEscFields[k]]) { var ue = decodeURIComponent(j); if (ue !== j) { doc[aceEscFields[k]][ue] = doc[aceEscFields[k]][j]; delete doc[aceEscFields[k]][j]; } } } } return doc; };
module.exports.aceUnEscapeAllFieldNames = function (docs) { for (var i in docs) { docs[i] = module.exports.aceUnEscapeFieldNames(docs[i]); } return docs; };
// Validation methods
module.exports.validateString = function (str, minlen, maxlen) { return ((str != null) && (typeof str == 'string') && ((minlen == null) || (str.length >= minlen)) && ((maxlen == null) || (str.length <= maxlen))); };
module.exports.validateInt = function (int, minval, maxval) { return ((int != null) && (typeof int == 'number') && ((minval == null) || (int >= minval)) && ((maxval == null) || (int <= maxval))); };
module.exports.validateArray = function (array, minlen, maxlen) { return ((array != null) && Array.isArray(array) && ((minlen == null) || (array.length >= minlen)) && ((maxlen == null) || (array.length <= maxlen))); };
module.exports.validateStrArray = function (array, minlen, maxlen) { if (((array != null) && Array.isArray(array)) == false) return false; for (var i in array) { if ( (typeof array[i] != 'string') || ((minlen != null) && (array[i].length < minlen)) || ((maxlen != null) && (array[i].length > maxlen))) return false; } return true; };
module.exports.validateObject = function (obj) { return ((obj != null) && (typeof obj == 'object')); };
module.exports.validateEmail = function (email, minlen, maxlen) { if (module.exports.validateString(email, minlen, maxlen) == false) return false; var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(email); };
module.exports.validateUsername = function (username, minlen, maxlen) { return (module.exports.validateString(username, minlen, maxlen) && (username.indexOf(' ') == -1) && (username.indexOf('"') == -1) && (username.indexOf(',') == -1)); };
module.exports.isAlphaNumeric = function (str) { return (str.match(/^[A-Za-z0-9]+$/) != null); };
module.exports.validateAlphaNumericArray = function (array, minlen, maxlen) { if (((array != null) && Array.isArray(array)) == false) return false; for (var i in array) { if ((typeof array[i] != 'string') || (module.exports.isAlphaNumeric(array[i]) == false) || ((minlen != null) && (array[i].length < minlen)) || ((maxlen != null) && (array[i].length > maxlen)) ) return false; } return true; };
module.exports.getEmailDomain = function(email) {
if (!module.exports.validateEmail(email, 1, 1024)) {
return '';
}
const i = email.indexOf('@');
return email.substring(i + 1).toLowerCase();
}
module.exports.validateEmailDomain = function(email, allowedDomains) {
// Check if this request is for an allows email domain
if ((allowedDomains != null) && Array.isArray(allowedDomains)) {
const emaildomain = module.exports.getEmailDomain(email);
if (emaildomain === '') {
return false;
}
var emailok = false;
for (var i in allowedDomains) { if (emaildomain == allowedDomains[i].toLowerCase()) { emailok = true; } }
return emailok;
}
return true;
}
// Validate that a string is a parseable http or https URL with a hostname
module.exports.validateUrl = function (url) {
if (!module.exports.validateString(url, 1, 4096)) return false;
try {
const u = new URL(url);
const scheme = (u.protocol || '').replace(/:$/, '').toLowerCase();
if (scheme !== 'http' && scheme !== 'https') return false;
if (!u.hostname || u.hostname.length === 0) return false;
return true;
} catch (ex) {
return false;
}
}
// Validate a remote image URL by checking headers and magic bytes (PNG/JPEG/GIF/WEBP/ICO)
// Returns a Promise that always resolves to true (valid image) or false (invalid/error) and never rejects.
module.exports.validateRemoteImage = function (url, options) {
options = options || {};
const timeoutMs = (typeof options.timeoutMs === 'number') ? options.timeoutMs : 5000;
const maxHeadBytes = (typeof options.maxHeadBytes === 'number') ? options.maxHeadBytes : 16384; // 16 KB
const maxContentLength = (typeof options.maxContentLength === 'number') ? options.maxContentLength : (5 * 1024 * 1024); // 5 MB
const allowedMimes = options.allowedMimes || ['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/x-icon', 'image/vnd.microsoft.icon', 'image/svg+xml'];
const agent = options.agent || undefined;
// This function MUST always resolve to boolean true/false and never reject.
return new Promise((resolve) => {
try {
if (!module.exports.validateUrl(url)) return resolve(false);
const http = require('http');
const https = require('https');
function doRequest(method, reqUrl, headers, redirectsLeft, cb) {
try {
const u = new URL(reqUrl);
const lib = (u.protocol === 'https:') ? https : http;
const reqOpts = { method: method, headers: headers || {}, timeout: timeoutMs };
if (agent) { reqOpts.agent = agent; }
const req = lib.request(u, reqOpts, (res) => {
// Follow redirects (3xx)
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirectsLeft > 0) {
res.resume();
const redirectUrl = new URL(res.headers.location, u).toString();
// Re-validate each redirect target
if (!module.exports.validateUrl(redirectUrl)) {
return cb(new Error('Invalid redirect URL'));
}
return doRequest(method, redirectUrl, headers, redirectsLeft - 1, cb);
}
cb(null, res, u.toString());
});
req.on('error', (e) => cb(e));
req.on('timeout', () => { req.destroy(new Error('timeout')); });
req.end();
} catch (ex) { cb(ex); }
}
// Centralized magic-byte detector
function detectBuffer(b) {
if (!b || b.length < 4) return null;
// PNG
if (b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4E && b[3] === 0x47 && b[4] === 0x0D && b[5] === 0x0A && b[6] === 0x1A && b[7] === 0x0A) return { mime: 'image/png', ext: 'png' };
// JPEG
if (b[0] === 0xFF && b[1] === 0xD8 && b[2] === 0xFF) return { mime: 'image/jpeg', ext: 'jpg' };
// GIF
if (b.length >= 6 && b.slice(0, 3).toString('ascii') === 'GIF') return { mime: 'image/gif', ext: 'gif' };
// WEBP (RIFF....WEBP)
if (b.length >= 12 && b.slice(0, 4).toString('ascii') === 'RIFF' && b.slice(8, 12).toString('ascii') === 'WEBP') return { mime: 'image/webp', ext: 'webp' };
// ICO (00 00 01 00)
if (b[0] === 0x00 && b[1] === 0x00 && b[2] === 0x01 && b[3] === 0x00) return { mime: 'image/x-icon', ext: 'ico' };
// SVG (text-based XML; look for '