master
js 633 lines 35.9 KB
Raw
1 /**
2 * @description MeshCentral Common Library
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 const fs = require('fs');
18 const crypto = require('crypto');
19 const path = require('path');
20 const { URL } = require('url');
21
22 // Binary encoding and decoding functions
23 module.exports.ReadShort = function (v, p) { return (v.charCodeAt(p) << 8) + v.charCodeAt(p + 1); };
24 module.exports.ReadShortX = function (v, p) { return (v.charCodeAt(p + 1) << 8) + v.charCodeAt(p); };
25 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.
26 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); };
27 module.exports.ShortToStr = function (v) { return String.fromCharCode((v >> 8) & 0xFF, v & 0xFF); };
28 module.exports.ShortToStrX = function (v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF); };
29 module.exports.IntToStr = function (v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); };
30 module.exports.IntToStrX = function (v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF); };
31 module.exports.MakeToArray = function (v) { if (!v || v == null || typeof v == 'object') return v; return [v]; };
32 module.exports.SplitArray = function (v) { return v.split(','); };
33 module.exports.Clone = function (v) { return JSON.parse(JSON.stringify(v)); };
34 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] != '.'); }; })();
35 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(''); }
36 module.exports.joinPath = function (base, path_) { return path.isAbsolute(path_) ? path_ : path.join(base, path_); }
37
38 // Move an element from one position in an array to a new position
39 module.exports.ArrayElementMove = function(arr, from, to) { arr.splice(to, 0, arr.splice(from, 1)[0]); };
40
41 // Format a string with arguments, "replaces {0} and {1}..."
42 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; }); };
43
44 // Print object for HTML
45 module.exports.ObjectToStringEx = function (x, c) {
46 var r = '', i;
47 if (x != 0 && (!x || x == null)) return "(Null)";
48 if (x instanceof Array) { for (i in x) { r += '<br />' + gap(c) + "Item #" + i + ": " + module.exports.ObjectToStringEx(x[i], c + 1); } }
49 else if (x instanceof Object) { for (i in x) { r += '<br />' + gap(c) + i + " = " + module.exports.ObjectToStringEx(x[i], c + 1); } }
50 else { r += x; }
51 return r;
52 };
53
54 // Print object for console
55 module.exports.ObjectToStringEx2 = function (x, c) {
56 var r = '', i;
57 if (x != 0 && (!x || x == null)) return "(Null)";
58 if (x instanceof Array) { for (i in x) { r += '\r\n' + gap2(c) + "Item #" + i + ": " + module.exports.ObjectToStringEx2(x[i], c + 1); } }
59 else if (x instanceof Object) { for (i in x) { r += '\r\n' + gap2(c) + i + " = " + module.exports.ObjectToStringEx2(x[i], c + 1); } }
60 else { r += x; }
61 return r;
62 };
63
64 // Create an ident gap
65 module.exports.gap = function (c) { var x = ''; for (var i = 0; i < (c * 4); i++) { x += '&nbsp;'; } return x; };
66 module.exports.gap2 = function (c) { var x = ''; for (var i = 0; i < (c * 4); i++) { x += ' '; } return x; };
67
68 // Print an object in html
69 module.exports.ObjectToString = function (x) { return module.exports.ObjectToStringEx(x, 0); };
70 module.exports.ObjectToString2 = function (x) { return module.exports.ObjectToStringEx2(x, 0); };
71
72 // Convert a hex string to a raw string
73 module.exports.hex2rstr = function (d) {
74 var r = '', m = ('' + d).match(/../g), t;
75 while (t = m.shift()) { r += String.fromCharCode('0x' + t); }
76 return r;
77 };
78
79 // Convert decimal to hex
80 module.exports.char2hex = function (i) { return (i + 0x100).toString(16).substr(-2).toUpperCase(); };
81
82 // Convert a raw string to a hex string
83 module.exports.rstr2hex = function (input) {
84 var r = '', i;
85 for (i = 0; i < input.length; i++) { r += module.exports.char2hex(input.charCodeAt(i)); }
86 return r;
87 };
88
89 // UTF-8 encoding & decoding functions
90 module.exports.encode_utf8 = function (s) { return unescape(encodeURIComponent(s)); };
91 module.exports.decode_utf8 = function (s) { return decodeURIComponent(escape(s)); };
92
93 // Convert a string into a blob
94 module.exports.data2blob = function (data) {
95 var bytes = new Array(data.length);
96 for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
97 var blob = new Blob([new Uint8Array(bytes)]);
98 return blob;
99 };
100
101 // Generate random numbers between 0 and max without bias.
102 module.exports.random = function (max) {
103 const crypto = require('crypto');
104 var maxmask = 1, r;
105 while (maxmask < max) { maxmask = (maxmask << 1) + 1; }
106 do { r = (crypto.randomBytes(4).readUInt32BE(0) & maxmask); } while (r > max);
107 return r;
108 };
109
110 // Split a comma separated string, ignoring commas in quotes.
111 module.exports.quoteSplit = function (str) {
112 var tmp = '', quote = 0, result = [];
113 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]; } }
114 if (tmp.length > 0) result.push(tmp.trim());
115 return result;
116 };
117
118 // Convert list of "name = value" into object
119 module.exports.parseNameValueList = function (list) {
120 var result = [];
121 for (var i in list) {
122 var j = list[i].indexOf('=');
123 if (j > 0) {
124 var v = list[i].substring(j + 1).trim();
125 if ((v[0] == '"') && (v[v.length - 1] == '"')) { v = v.substring(1, v.length - 1); }
126 result[list[i].substring(0, j).trim()] = v;
127 }
128 }
129 return result;
130 };
131
132 // Compute the MD5 digest hash for a set of values
133 module.exports.ComputeDigesthash = function (username, password, realm, method, path, qop, nonce, nc, cnonce) {
134 var ha1 = crypto.createHash('md5').update(username + ":" + realm + ":" + password).digest('hex');
135 var ha2 = crypto.createHash('md5').update(method + ":" + path).digest('hex');
136 return crypto.createHash('md5').update(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2).digest("hex");
137 };
138
139 module.exports.toNumber = function (str) { var x = parseInt(str); if (x == str) return x; return str; };
140 module.exports.escapeHtml = function (string) { return String(string).replace(/[&<>"'`=\/]/g, function (s) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;' }[s]; }); };
141 module.exports.escapeHtmlBreaks = function (string) { return String(string).replace(/[&<>"'`=\/]/g, function (s) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;', '\r': '<br />', '\n': '' }[s]; }); };
142 module.exports.zeroPad = function(num, c) { if (c == null) { c = 2; } var s = '000000' + num; return s.substr(s.length - c); }
143
144 // Lowercase all the names in a object recursively
145 // Allow for exception keys, child of exceptions will not get lower-cased.
146 // Exceptions is an array of "keyname" or "parent\keyname"
147 module.exports.objKeysToLower = function (obj, exceptions, parent) {
148 for (var i in obj) {
149 if ((typeof obj[i] == 'object') &&
150 ((exceptions == null) || (exceptions.indexOf(i.toLowerCase()) == -1) && ((parent == null) || (exceptions.indexOf(parent.toLowerCase() + '/' + i.toLowerCase()) == -1)))
151 ) {
152 module.exports.objKeysToLower(obj[i], exceptions, i); // LowerCase all key names in the child object
153 }
154 if (i.toLowerCase() !== i) { obj[i.toLowerCase()] = obj[i]; delete obj[i]; } // LowerCase all key names
155 }
156 return obj;
157 };
158
159 // 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)
160 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'); };
161 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('%'); };
162
163 // Escape all links, SSH and RDP usernames
164 // This is required for databases like NeDB that don't accept "." or "," as part of a field name.
165 module.exports.escapeLinksFieldNameEx = function (docx) { if ((docx.links == null) && (docx.ssh == null) && (docx.rdp == null)) { return docx; } return module.exports.escapeLinksFieldName(docx); };
166 module.exports.escapeLinksFieldName = function (docx) {
167 var doc = Object.assign({}, docx);
168 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]; } } }
169 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]; } } }
170 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]; } } }
171 return doc;
172 };
173 module.exports.unEscapeLinksFieldName = function (doc) {
174 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]; } } }
175 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]; } } }
176 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]; } } }
177 return doc;
178 };
179 //module.exports.escapeAllLinksFieldName = function (docs) { for (var i in docs) { module.exports.escapeLinksFieldName(docs[i]); } return docs; };
180 module.exports.unEscapeAllLinksFieldName = function (docs) { for (var i in docs) { docs[i] = module.exports.unEscapeLinksFieldName(docs[i]); } return docs; };
181
182 // Escape field names for aceBase
183 var aceEscFields = ['links', 'ssh', 'rdp', 'notify'];
184 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; };
185 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; };
186 module.exports.aceUnEscapeAllFieldNames = function (docs) { for (var i in docs) { docs[i] = module.exports.aceUnEscapeFieldNames(docs[i]); } return docs; };
187
188 // Validation methods
189 module.exports.validateString = function (str, minlen, maxlen) { return ((str != null) && (typeof str == 'string') && ((minlen == null) || (str.length >= minlen)) && ((maxlen == null) || (str.length <= maxlen))); };
190 module.exports.validateInt = function (int, minval, maxval) { return ((int != null) && (typeof int == 'number') && ((minval == null) || (int >= minval)) && ((maxval == null) || (int <= maxval))); };
191 module.exports.validateArray = function (array, minlen, maxlen) { return ((array != null) && Array.isArray(array) && ((minlen == null) || (array.length >= minlen)) && ((maxlen == null) || (array.length <= maxlen))); };
192 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; };
193 module.exports.validateObject = function (obj) { return ((obj != null) && (typeof obj == 'object')); };
194 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); };
195 module.exports.validateUsername = function (username, minlen, maxlen) { return (module.exports.validateString(username, minlen, maxlen) && (username.indexOf(' ') == -1) && (username.indexOf('"') == -1) && (username.indexOf(',') == -1)); };
196 module.exports.isAlphaNumeric = function (str) { return (str.match(/^[A-Za-z0-9]+$/) != null); };
197 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; };
198 module.exports.getEmailDomain = function(email) {
199 if (!module.exports.validateEmail(email, 1, 1024)) {
200 return '';
201 }
202 const i = email.indexOf('@');
203 return email.substring(i + 1).toLowerCase();
204 }
205
206 module.exports.validateEmailDomain = function(email, allowedDomains) {
207 // Check if this request is for an allows email domain
208 if ((allowedDomains != null) && Array.isArray(allowedDomains)) {
209 const emaildomain = module.exports.getEmailDomain(email);
210 if (emaildomain === '') {
211 return false;
212 }
213 var emailok = false;
214 for (var i in allowedDomains) { if (emaildomain == allowedDomains[i].toLowerCase()) { emailok = true; } }
215 return emailok;
216 }
217
218 return true;
219 }
220
221 // Validate that a string is a parseable http or https URL with a hostname
222 module.exports.validateUrl = function (url) {
223 if (!module.exports.validateString(url, 1, 4096)) return false;
224 try {
225 const u = new URL(url);
226 const scheme = (u.protocol || '').replace(/:$/, '').toLowerCase();
227 if (scheme !== 'http' && scheme !== 'https') return false;
228 if (!u.hostname || u.hostname.length === 0) return false;
229 return true;
230 } catch (ex) {
231 return false;
232 }
233 }
234
235 // Validate a remote image URL by checking headers and magic bytes (PNG/JPEG/GIF/WEBP/ICO)
236 // Returns a Promise<boolean> that always resolves to true (valid image) or false (invalid/error) and never rejects.
237 module.exports.validateRemoteImage = function (url, options) {
238 options = options || {};
239 const timeoutMs = (typeof options.timeoutMs === 'number') ? options.timeoutMs : 5000;
240 const maxHeadBytes = (typeof options.maxHeadBytes === 'number') ? options.maxHeadBytes : 16384; // 16 KB
241 const maxContentLength = (typeof options.maxContentLength === 'number') ? options.maxContentLength : (5 * 1024 * 1024); // 5 MB
242 const allowedMimes = options.allowedMimes || ['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/x-icon', 'image/vnd.microsoft.icon', 'image/svg+xml'];
243 const agent = options.agent || undefined;
244 // This function MUST always resolve to boolean true/false and never reject.
245 return new Promise((resolve) => {
246 try {
247 if (!module.exports.validateUrl(url)) return resolve(false);
248
249 const http = require('http');
250 const https = require('https');
251
252 function doRequest(method, reqUrl, headers, redirectsLeft, cb) {
253 try {
254 const u = new URL(reqUrl);
255 const lib = (u.protocol === 'https:') ? https : http;
256 const reqOpts = { method: method, headers: headers || {}, timeout: timeoutMs };
257 if (agent) { reqOpts.agent = agent; }
258 const req = lib.request(u, reqOpts, (res) => {
259 // Follow redirects (3xx)
260 if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirectsLeft > 0) {
261 res.resume();
262 const redirectUrl = new URL(res.headers.location, u).toString();
263 // Re-validate each redirect target
264 if (!module.exports.validateUrl(redirectUrl)) {
265 return cb(new Error('Invalid redirect URL'));
266 }
267 return doRequest(method, redirectUrl, headers, redirectsLeft - 1, cb);
268 }
269 cb(null, res, u.toString());
270 });
271 req.on('error', (e) => cb(e));
272 req.on('timeout', () => { req.destroy(new Error('timeout')); });
273 req.end();
274 } catch (ex) { cb(ex); }
275 }
276
277 // Centralized magic-byte detector
278 function detectBuffer(b) {
279 if (!b || b.length < 4) return null;
280 // PNG
281 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' };
282 // JPEG
283 if (b[0] === 0xFF && b[1] === 0xD8 && b[2] === 0xFF) return { mime: 'image/jpeg', ext: 'jpg' };
284 // GIF
285 if (b.length >= 6 && b.slice(0, 3).toString('ascii') === 'GIF') return { mime: 'image/gif', ext: 'gif' };
286 // WEBP (RIFF....WEBP)
287 if (b.length >= 12 && b.slice(0, 4).toString('ascii') === 'RIFF' && b.slice(8, 12).toString('ascii') === 'WEBP') return { mime: 'image/webp', ext: 'webp' };
288 // ICO (00 00 01 00)
289 if (b[0] === 0x00 && b[1] === 0x00 && b[2] === 0x01 && b[3] === 0x00) return { mime: 'image/x-icon', ext: 'ico' };
290 // SVG (text-based XML; look for '<svg' or '<?xml' after optional whitespace/BOM)
291 try {
292 var svgHead = b.slice(0, Math.min(b.length, 256)).toString('utf8').trimStart();
293 if (svgHead.charCodeAt(0) === 0xFEFF) { svgHead = svgHead.slice(1); } // strip BOM
294 if (svgHead.startsWith('<svg') || svgHead.startsWith('<?xml')) return { mime: 'image/svg+xml', ext: 'svg' };
295 } catch (e) {}
296 return null;
297 }
298
299 // Inspect an incoming GET response stream for image magic bytes.
300 function inspectStreamForImage(getRes) {
301 return new Promise((resolveInspect) => {
302 try {
303 const chunks = [];
304 let length = 0;
305 let timedOut = false;
306 let finished = false;
307 const to = setTimeout(() => { timedOut = true; try { getRes.destroy(new Error('timeout')); } catch (e) {} }, timeoutMs + 1000);
308
309 function finalize(result) {
310 if (finished) return;
311 finished = true;
312 clearTimeout(to);
313 try { getRes.removeAllListeners('data'); getRes.removeAllListeners('end'); getRes.removeAllListeners('close'); getRes.removeAllListeners('error'); } catch (e) {}
314 try { return resolveInspect(result); } catch (e) { return; }
315 }
316
317 getRes.on('data', (chunk) => {
318 if (timedOut || finished) return;
319 if (length < maxHeadBytes) {
320 const remaining = maxHeadBytes - length;
321 if (chunk.length <= remaining) {
322 chunks.push(chunk);
323 length += chunk.length;
324 } else {
325 chunks.push(chunk.slice(0, remaining));
326 length += remaining;
327 }
328 }
329 if (length >= maxHeadBytes) {
330 try {
331 const buf = Buffer.concat(chunks, Math.min(length, maxHeadBytes));
332 const det = detectBuffer(buf);
333 if (!det) finalize(false);
334 else if (allowedMimes.indexOf(det.mime) === -1) finalize(false);
335 else finalize(true);
336 } catch (ex) { finalize(false); }
337 try { getRes.destroy(); } catch (e) {}
338 }
339 });
340
341 getRes.on('end', () => {
342 if (finished) return;
343 try {
344 const buf = Buffer.concat(chunks, Math.min(length, maxHeadBytes));
345 const det = detectBuffer(buf);
346 if (!det) finalize(false);
347 else if (allowedMimes.indexOf(det.mime) === -1) finalize(false);
348 else finalize(true);
349 } catch (ex) { finalize(false); }
350 });
351
352 getRes.on('close', () => { if (!finished) finalize(false); });
353 getRes.on('error', (e) => { if (!finished) finalize(false); });
354 } catch (ex) { try { return resolveInspect(false); } catch (e) {} }
355 });
356 }
357
358 // First do a HEAD to validate content-type/length. Some hosts/CDNs don't
359 // support HEAD (returning 405/403/501) even though GET would work. In
360 // that case, fall back to a small ranged GET to validate magic bytes.
361 doRequest('HEAD', url, { 'User-Agent': 'MeshCentral/validateRemoteImage' }, 5, (err, headRes, finalUrl) => {
362 // If doRequest failed to produce a finalUrl (network error),
363 // fall back to the original requested URL to allow GET to be
364 // attempted when HEAD fails with network/DNS issues.
365 const effectiveUrl = finalUrl || url;
366 try {
367 // If HEAD errored or returned a client/server error
368 if (err || (headRes && headRes.statusCode >= 400)) {
369 // If the status suggests HEAD is not allowed/implemented or forbidden,
370 // attempt a ranged GET fallback. For other 4xx/5xx responses, fail fast.
371 const status = headRes && headRes.statusCode ? headRes.statusCode : 0;
372 if (err || status === 405 || status === 501 || status === 403) {
373 try { if (headRes && headRes.resume) headRes.resume(); } catch (e) {}
374 const headers = { 'Range': 'bytes=0-' + (maxHeadBytes - 1), 'User-Agent': 'MeshCentral/validateRemoteImage' };
375 // Ranged GET fallback: reuse the same GET inspection logic.
376 // Use the effectiveUrl (finalUrl or original url) in case
377 // the HEAD redirect wasn't available.
378 doRequest('GET', effectiveUrl, headers, 5, (err2, getRes) => {
379 try {
380 if (err2) return resolve(false);
381 if (getRes.statusCode !== 200 && getRes.statusCode !== 206) { getRes.resume(); return resolve(false); }
382 inspectStreamForImage(getRes).then((r) => resolve(r)).catch(() => resolve(false));
383 } catch (ex) { return resolve(false); }
384 });
385 return;
386 }
387 // Other HTTP errors from HEAD should fail fast
388 try { if (headRes && headRes.resume) headRes.resume(); } catch (e) {}
389 return resolve(false);
390 }
391 const ct = (headRes.headers['content-type'] || '').toLowerCase();
392 const clen = parseInt(headRes.headers['content-length'] || '0', 10) || 0;
393 if (clen > 0 && clen > maxContentLength) { headRes.resume(); return resolve(false); }
394 if (ct && (allowedMimes.indexOf(ct.split(';')[0].trim()) === -1)) { headRes.resume(); return resolve(false); }
395 // Now fetch a partial range to inspect magic bytes
396 const headers = { 'Range': 'bytes=0-' + (maxHeadBytes - 1), 'User-Agent': 'MeshCentral/validateRemoteImage' };
397 // Drain any potential body from the HEAD response so the
398 // socket can be reused and not held open when issuing the GET.
399 try { if (headRes && headRes.resume) headRes.resume(); } catch (e) {}
400 doRequest('GET', effectiveUrl, headers, 5, (err2, getRes) => {
401 try {
402 if (err2) return resolve(false);
403 if (getRes.statusCode !== 200 && getRes.statusCode !== 206) { getRes.resume(); return resolve(false); }
404 inspectStreamForImage(getRes).then((r) => resolve(r)).catch(() => resolve(false));
405 } catch (ex) { return resolve(false); }
406 });
407 } catch (ex) { return resolve(false); }
408 });
409 } catch (ex) {
410 return resolve(false);
411 }
412 });
413 }
414
415 // Check password requirements
416 module.exports.checkPasswordRequirements = function(password, requirements) {
417 if ((requirements == null) || (requirements == '') || (typeof requirements != 'object')) return true;
418 if (requirements.min) { if (password.length < requirements.min) return false; }
419 if (requirements.max) { if (password.length > requirements.max) return false; }
420 var numeric = 0, lower = 0, upper = 0, nonalpha = 0;
421 for (var i = 0; i < password.length; i++) {
422 if (/\d/.test(password[i])) { numeric++; }
423 if (/[a-z]/.test(password[i])) { lower++; }
424 if (/[A-Z]/.test(password[i])) { upper++; }
425 if (/\W/.test(password[i])) { nonalpha++; }
426 }
427 if (requirements.numeric && (numeric < requirements.numeric)) return false;
428 if (requirements.lower && (lower < requirements.lower)) return false;
429 if (requirements.upper && (upper < requirements.upper)) return false;
430 if (requirements.nonalpha && (nonalpha < requirements.nonalpha)) return false;
431 return true;
432 }
433
434
435 // Limits the number of tasks running to a fixed limit placing the rest in a pending queue.
436 // This is useful to limit the number of agents upgrading at the same time, to not swamp
437 // the network with traffic.
438 module.exports.createTaskLimiterQueue = function (maxTasks, maxTaskTime, cleaningInterval) {
439 var obj = { maxTasks: maxTasks, maxTaskTime: (maxTaskTime * 1000), nextTaskId: 0, currentCount: 0, current: {}, pending: [[], [], []], timer: null };
440
441 // Add a task to the super queue
442 // Priority: 0 = High, 1 = Medium, 2 = Low
443 obj.launch = function (func, arg, pri) {
444 if (typeof pri != 'number') { pri = 2; }
445 if (obj.currentCount < obj.maxTasks) {
446 // Run this task now
447 const id = obj.nextTaskId++;
448 obj.current[id] = Date.now() + obj.maxTaskTime;
449 obj.currentCount++;
450 //console.log('ImmidiateLaunch ' + id);
451 func(arg, id, obj); // Start the task
452 if (obj.timer == null) { obj.timer = setInterval(obj.clean, cleaningInterval * 1000); }
453 } else {
454 // Hold this task
455 //console.log('Holding');
456 obj.pending[pri].push({ func: func, arg: arg });
457 }
458 }
459
460 // Called when a task is completed
461 obj.completed = function (taskid) {
462 //console.log('Completed ' + taskid);
463 if (obj.current[taskid]) { delete obj.current[taskid]; obj.currentCount--; } else { return; }
464 while ((obj.currentCount < obj.maxTasks) && ((obj.pending[0].length > 0) || (obj.pending[1].length > 0) || (obj.pending[2].length > 0))) {
465 // Run this task now
466 var t = null;
467 if (obj.pending[0].length > 0) { t = obj.pending[0].shift(); }
468 else if (obj.pending[1].length > 0) { t = obj.pending[1].shift(); }
469 else if (obj.pending[2].length > 0) { t = obj.pending[2].shift(); }
470 const id = obj.nextTaskId++;
471 obj.current[id] = Date.now() + obj.maxTaskTime;
472 obj.currentCount++;
473 //console.log('PendingLaunch ' + id);
474 t.func(t.arg, id, obj); // Start the task
475 }
476 if ((obj.currentCount == 0) && (obj.pending[0].length == 0) && (obj.pending[1].length == 0) && (obj.pending[2].length == 0) && (obj.timer != null)) {
477 // All done, clear the timer
478 clearInterval(obj.timer); obj.timer = null;
479 }
480 }
481
482 // Look for long standing tasks and clean them up
483 obj.clean = function () {
484 const t = Date.now();
485 for (var i in obj.current) { if (obj.current[i] < t) { obj.completed(parseInt(i)); } }
486 }
487
488 return obj;
489 }
490
491 // Convert string translations to a standardized JSON we can use in GitHub
492 // Strings are sorder by english source and object keys are sorted
493 module.exports.translationsToJson = function(t) {
494 var arr2 = [], arr = t.strings;
495 for (var i in arr) {
496 var names = [], el = arr[i], el2 = {};
497 for (var j in el) { names.push(j); }
498 names.sort(function (a, b) { if (a == b) { return 0; } if (a == 'xloc') { return 1; } if (b == 'xloc') { return -1; } return a - b });
499 for (var j in names) { el2[names[j]] = el[names[j]]; }
500 if (el2.xloc != null) { el2.xloc.sort(); }
501 arr2.push(el2);
502 }
503 arr2.sort(function (a, b) { if (a.en > b.en) return 1; if (a.en < b.en) return -1; return 0; });
504 return JSON.stringify({ strings: arr2 }, null, ' ');
505 }
506
507 module.exports.copyFile = function(source, target, cb) {
508 var cbCalled = false, rd = fs.createReadStream(source);
509 rd.on('error', function (err) { done(err); });
510 var wr = fs.createWriteStream(target);
511 wr.on('error', function (err) { done(err); });
512 wr.on('close', function (ex) { done(); });
513 rd.pipe(wr);
514 function done(err) { if (!cbCalled) { cb(err); cbCalled = true; } }
515 }
516
517 module.exports.meshServerRightsArrayToNumber = function (val) {
518 if (val == null) return null;
519 if (typeof val == 'number') return val;
520 if (Array.isArray(val)) {
521 var newAccRights = 0;
522 for (var j in val) {
523 var r = val[j].toLowerCase();
524 if (r == 'fulladmin') { newAccRights = 4294967295; } // 0xFFFFFFFF
525 if (r == 'serverbackup') { newAccRights |= 1; }
526 if (r == 'manageusers') { newAccRights |= 2; }
527 if (r == 'serverrestore') { newAccRights |= 4; }
528 if (r == 'fileaccess') { newAccRights |= 8; }
529 if (r == 'serverupdate') { newAccRights |= 16; }
530 if (r == 'locked') { newAccRights |= 32; }
531 if (r == 'nonewgroups') { newAccRights |= 64; }
532 if (r == 'notools') { newAccRights |= 128; }
533 if (r == 'usergroups') { newAccRights |= 256; }
534 if (r == 'recordings') { newAccRights |= 512; }
535 if (r == 'locksettings') { newAccRights |= 1024; }
536 if (r == 'allevents') { newAccRights |= 2048; }
537 if (r == 'nonewdevices') { newAccRights |= 4096; }
538 }
539 return newAccRights;
540 }
541 return null;
542 }
543
544 // Sort an object by key
545 module.exports.sortObj = function (obj) { return Object.keys(obj).sort().reduce(function (result, key) { result[key] = obj[key]; return result; }, {}); }
546
547 // Validate an object to make sure it can be stored in MongoDB
548 module.exports.validateObjectForMongo = function (obj, maxStrLen) {
549 return validateObjectForMongoRec(obj, maxStrLen);
550 }
551
552 function validateObjectForMongoRec(obj, maxStrLen) {
553 if (typeof obj != 'object') return false;
554 for (var i in obj) {
555 // Check the key name is not too long
556 if (i.length > 100) return false;
557 // Check if all chars are alpha-numeric or underscore.
558 for (var j in i) { const c = i.charCodeAt(j); if ((c < 48) || ((c > 57) && (c < 65)) || ((c > 90) && (c < 97) && (c != 95)) || (c > 122)) return false; }
559 // If the value is a string, check it's not too long
560 if ((typeof obj[i] == 'string') && (obj[i].length > maxStrLen)) return false;
561 // If the value is an object, check it.
562 if ((typeof obj[i] == 'object') && (Array.isArray(obj[i]) == false) && (validateObjectForMongoRec(obj[i], maxStrLen) == false)) return false;
563 }
564 return true;
565 }
566
567 // Parse a version string of the type n.n.n.n
568 module.exports.parseVersion = function (verstr) {
569 if (typeof verstr != 'string') return null;
570 const r = [], verstrsplit = verstr.split('.');
571 if (verstrsplit.length != 4) return null;
572 for (var i in verstrsplit) {
573 var n = parseInt(verstrsplit[i]);
574 if (isNaN(n) || (n < 0) || (n > 65535)) return null;
575 r.push(n);
576 }
577 return r;
578 }
579
580 // Move old files. If we are about to overwrite a file, we can move if first just in case the change needs to be reverted
581 module.exports.moveOldFiles = function (filelist) {
582 // Fine an old extension that works for all files in the file list
583 var oldFileExt, oldFileExtCount = 0, extOk;
584 do {
585 extOk = true;
586 if (++oldFileExtCount == 1) { oldFileExt = '-old'; } else { oldFileExt = '-old' + oldFileExtCount; }
587 for (var i in filelist) { if (fs.existsSync(filelist[i] + oldFileExt) == true) { extOk = false; } }
588 } while (extOk == false);
589 for (var i in filelist) { try { fs.renameSync(filelist[i], filelist[i] + oldFileExt); } catch (ex) { } }
590 }
591
592 // Convert strArray to Array, returns array if strArray or null if any other type
593 module.exports.convertStrArray = function (object, split) {
594 if (split && typeof object === 'string') {
595 return object.split(split)
596 } else if (typeof object === 'string') {
597 return Array(object);
598 } else if (Array.isArray(object)) {
599 return object
600 } else {
601 return []
602 }
603 }
604
605 module.exports.uniqueArray = function (a) {
606 var seen = {};
607 var out = [];
608 var len = a.length;
609 var j = 0;
610 for(var i = 0; i < len; i++) {
611 var item = a[i];
612 if(seen[item] !== 1) {
613 seen[item] = 1;
614 out[j++] = item;
615 }
616 }
617 return out;
618 }
619
620 // Replace placeholders in a string with values from an object or a function
621 module.exports.replacePlaceholders = function (template, values) {
622 return template.replace(/\{(\w+)\}/g, (match, key) => {
623 if (typeof values === 'function') {
624 return values(key);
625 }
626 else if (values && typeof values === 'object') {
627 return values[key] !== undefined ? values[key] : match;
628 }
629 else {
630 return values !== undefined ? values : match;
631 }
632 });
633 }