39
obj.net = require('net');
40
obj.tls = require('tls');
41
obj.path = require('path');
42
+ obj.os = require('os');
43
obj.bodyParser = require('body-parser');
44
obj.exphbs = require('express-handlebars');
45
obj.crypto = require('crypto');
99
obj.renderLanguages = [];
100
obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time
101
102
+ const isWindowsPlatform = (obj.os.platform() === 'win32');
103
+ const safeUploadTempRoots = (function () {
104
+ const roots = [];
105
+ const addRoot = function (p) {
106
+ if (typeof p !== 'string') { return; }
107
+ var resolved;
108
+ try { resolved = obj.path.normalize(obj.path.resolve(p)); } catch (ex) { return; }
109
+ if (resolved.length === 0) { return; }
110
+ if ((resolved.length > 1) && resolved.endsWith(obj.path.sep)) { resolved = resolved.slice(0, -1); }
111
+ const comparison = isWindowsPlatform ? resolved.toLowerCase() : resolved;
112
+ const comparisonWithSep = comparison + obj.path.sep;
113
+ roots.push({ comparison: comparison, comparisonWithSep: comparisonWithSep });
114
+ };
115
+ addRoot(obj.os.tmpdir());
116
+ if (typeof obj.parent.filespath === 'string') { addRoot(obj.path.join(obj.parent.filespath, 'tmp')); }
117
+ return roots;
118
+ })();
119
+ function resolveSafeUploadTempPath(tempPath) {
120
+ if (typeof tempPath !== 'string') { return null; }
121
+ var resolvedPath;
122
+ try { resolvedPath = obj.path.normalize(obj.path.resolve(tempPath)); } catch (ex) { return null; }
123
+ var comparisonPath = isWindowsPlatform ? resolvedPath.toLowerCase() : resolvedPath;
124
+ var comparisonPathNoTrailing = comparisonPath;
125
+ if ((comparisonPathNoTrailing.length > 1) && comparisonPathNoTrailing.endsWith(obj.path.sep)) { comparisonPathNoTrailing = comparisonPathNoTrailing.slice(0, -1); }
126
+ for (var i = 0; i < safeUploadTempRoots.length; i++) {
127
+ var root = safeUploadTempRoots[i];
128
+ if ((comparisonPathNoTrailing === root.comparison) || comparisonPath.startsWith(root.comparisonWithSep)) { return resolvedPath; }
129
+ }
130
+ return null;
131
+ }
132
+
133
// Web relay sessions
134
var webRelayNextSessionId = 1;
135
var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session
2157
}
2158
}
2159
}
2128
- });
2160
+ });
2161
}
2162
} else {
2163
render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 10, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
2913
res.set('Content-Type', 'text/html');
2914
let url = domain.url;
2915
if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); }
2884
-
2916
+
2917
// check for relaystate is set, test against configured server name and accepted query params
2918
if(req.body && req.body.RelayState !== undefined){
2919
var relayState = decodeURIComponent(req.body.RelayState);
2920
var serverName = (obj.getWebServerName(domain, req)).replaceAll('.','\\.');
2889
-
2921
+
2922
var regexstr = `(?<=https:\\/\\/(?:.+?\\.)?${serverName}\\/?)` +
2923
`.*((?<=([\\?&])gotodevicename=(.{64})|` +
2924
`gotonode=(.{64})|` +
2938
`webrtc=|` +
2939
`hide=|` +
2940
`viewmode=(\\d+)(?=[\\&]|\\b)))`;
2909
-
2941
+
2942
var regex = new RegExp(regexstr);
2943
if(regex.test(relayState)){
2944
url = relayState;
2945
}
2946
}
2915
-
2947
+
2948
res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
2949
}
2950
3266
// Get WebRTC configuration
3267
var webRtcConfig = null;
3268
if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); }
3237
- else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
3269
+ else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
3270
3271
// Load default page style or new modern ui
3272
var uiViewMode = 'default';
3300
customui: customui,
3301
customFiles: customFiles,
3302
webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
3271
- footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, {
3303
+ footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, {
3304
'serverversion': obj.parent.currentVer,
3305
'servername': obj.getWebServerName(domain, req),
3306
'agentsessions': Object.keys(parent.webserver.wsagents).length,
3610
messageid: msgid,
3611
flashErrors: JSON.stringify(flashErrors).replace(/"/g, '\\"'),
3612
passhint: passhint,
3581
-
3582
- welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, {
3613
+
3614
+ welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, {
3615
'serverversion': obj.parent.currentVer,
3616
'servername': obj.getWebServerName(domain, req),
3617
'agentsessions': Object.keys(parent.webserver.wsagents).length,
4583
const nodeid = fields.attrib[0];
4584
obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
4585
if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4554
- files.files.forEach(function (file) {
4555
- obj.fs.readFile(file.path, 'utf8', function (err, data) {
4586
+ for (var i in files.files) {
4587
+ var file = files.files[i];
4588
+ const uploadTempPath = resolveSafeUploadTempPath(file.path);
4589
+ if (uploadTempPath == null) { res.sendStatus(400); return; }
4590
+ obj.fs.readFile(uploadTempPath, 'utf8', function (err, data) {
4591
if (err != null) return;
4592
data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
4593
obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
4559
- try { obj.fs.unlinkSync(file.path); } catch (e) { }
4594
+ try { obj.fs.unlinkSync(uploadTempPath); } catch (e) { }
4595
});
4561
- });
4596
+ }
4597
res.send('');
4598
});
4599
});
4628
const nodeid = fields.attrib[0];
4629
obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
4630
if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4596
- files.files.forEach(function (file) {
4631
+ for (var i in files.files) {
4632
+ var file = files.files[i];
4633
+ const uploadTempPath = resolveSafeUploadTempPath(file.path);
4634
+ if (uploadTempPath == null) { res.sendStatus(400); return; }
4635
+
4636
// Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
4598
- parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.path });
4637
+ parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: uploadTempPath });
4638
4600
- //try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
4601
- });
4639
+ //try { obj.fs.unlinkSync(uploadTempPath); } catch (e) { } // TODO: Remove this file after 30 minutes.
4640
+ }
4641
res.send('');
4642
});
4643
});
4644
}
4645
4646
// Upload a file to the server
4647
+ function getCustomIconUserKey(user) {
4648
+ if ((user == null) || (typeof user._id !== 'string') || (user._id.length === 0)) { return null; }
4649
+ return obj.crypto.createHash('sha256').update(user._id).digest('hex');
4650
+ }
4651
+
4652
+ function getCustomIconUserDir(user) {
4653
+ const userKey = getCustomIconUserKey(user);
4654
+ if (userKey == null) { return null; }
4655
+ return obj.path.join(obj.parent.datapath, 'icons', 'custom', userKey);
4656
+ }
4657
+
4658
+ function handleCustomIconUpload(req, res) {
4659
+ const domain = checkUserIpAddress(req, res);
4660
+ if (domain == null) { return; }
4661
+ if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; }
4662
+ const user = obj.users[req.session.userid];
4663
+ if (user == null) { res.sendStatus(401); return; }
4664
+
4665
+ const multiparty = require('multiparty');
4666
+ const form = new multiparty.Form();
4667
+ form.parse(req, function (err, fields, files) {
4668
+ if (err) { res.status(400).json({ success: false, error: 'Invalid form submission.' }); return; }
4669
+
4670
+ const allowedTypes = { myDevices: 1, myAccount: 1, myEvents: 1, myFiles: 1, myUsers: 1, myServer: 1 };
4671
+ const iconType = (fields && fields.iconType && fields.iconType[0]) ? fields.iconType[0] : null;
4672
+ if ((typeof iconType !== 'string') || (allowedTypes[iconType] !== 1)) { res.status(400).json({ success: false, error: 'Invalid icon type.' }); return; }
4673
+
4674
+ const iconFile = (files && files.iconFile && files.iconFile[0]) ? files.iconFile[0] : null;
4675
+ if ((iconFile == null) || (typeof iconFile.path !== 'string')) { res.status(400).json({ success: false, error: 'Missing icon file.' }); return; }
4676
+ const iconTempPath = resolveSafeUploadTempPath(iconFile.path);
4677
+ if (iconTempPath == null) { res.status(400).json({ success: false, error: 'Invalid icon file location.' }); return; }
4678
+
4679
+ const cleanupTempFile = function () { try { obj.fs.unlink(iconTempPath, function () { }); } catch (ex) { } };
4680
+
4681
+ const extension = obj.path.extname(iconFile.originalFilename || '').toLowerCase();
4682
+ if ((extension !== '.svg') && (extension !== '.png')) { cleanupTempFile(); res.status(400).json({ success: false, error: 'Only SVG and PNG files are supported.' }); return; }
4683
+
4684
+ const iconsRoot = obj.path.join(obj.parent.datapath, 'icons');
4685
+ const customDir = obj.path.join(iconsRoot, 'custom');
4686
+ const userCustomDir = getCustomIconUserDir(user);
4687
+ const userKey = getCustomIconUserKey(user);
4688
+ if ((userCustomDir == null) || (userKey == null)) { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare user icons directory.' }); return; }
4689
+ try { obj.fs.mkdirSync(iconsRoot); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare icons directory.' }); return; } }
4690
+ try { obj.fs.mkdirSync(customDir); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare icons directory.' }); return; } }
4691
+ try { obj.fs.mkdirSync(userCustomDir); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare user icons directory.' }); return; } }
4692
+
4693
+ const previousIcon = (fields && fields.previousIcon && fields.previousIcon[0]) ? fields.previousIcon[0] : null;
4694
+ const previousInfo = resolveCustomIconPath(previousIcon, user);
4695
+ if ((previousInfo != null) && (previousInfo.isOwned === true)) {
4696
+ try { obj.fs.unlinkSync(previousInfo.diskPath); } catch (ex) { }
4697
+ }
4698
+
4699
+ const newFilename = iconType + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8) + extension;
4700
+ const destinationPath = obj.path.join(userCustomDir, newFilename);
4701
+
4702
+ const respondSuccess = function () { res.json({ success: true, path: domain.url + 'icons/custom/' + userKey + '/' + newFilename }); };
4703
+
4704
+ obj.fs.rename(iconTempPath, destinationPath, function (renameErr) {
4705
+ if (renameErr == null) { respondSuccess(); return; }
4706
+ if ((renameErr != null) && (renameErr.code === 'EXDEV')) {
4707
+ obj.common.copyFile(iconTempPath, destinationPath, function (copyErr) {
4708
+ cleanupTempFile();
4709
+ if (copyErr) { res.status(500).json({ success: false, error: 'Failed to save uploaded icon.' }); return; }
4710
+ respondSuccess();
4711
+ });
4712
+ } else {
4713
+ cleanupTempFile();
4714
+ res.status(500).json({ success: false, error: 'Failed to save uploaded icon.' });
4715
+ }
4716
+ });
4717
+ });
4718
+ }
4719
+
4720
+ function resolveCustomIconPath(requestPath, user) {
4721
+ if (typeof requestPath !== 'string') { return null; }
4722
+ if (requestPath.startsWith('http://') || requestPath.startsWith('https://') || requestPath.startsWith('data:')) { return null; }
4723
+ const pathOnly = requestPath.split('?')[0].split('#')[0];
4724
+ const marker = '/icons/custom/';
4725
+ const markerIndex = pathOnly.indexOf(marker);
4726
+ if (markerIndex < 0) { return null; }
4727
+ const relativePath = pathOnly.substring(markerIndex + marker.length);
4728
+ if ((relativePath.length === 0) || (relativePath.indexOf('\\') !== -1)) { return null; }
4729
+ const pathParts = relativePath.split('/');
4730
+ if ((pathParts.length !== 1) && (pathParts.length !== 2)) { return null; }
4731
+ for (var i = 0; i < pathParts.length; i++) {
4732
+ if ((pathParts[i].length === 0) || (obj.common.IsFilenameValid(pathParts[i]) !== true)) { return null; }
4733
+ }
4734
+
4735
+ var ownerKey = null, iconName = null, diskPath = null, isOwned = false;
4736
+ const iconsRoot = obj.path.join(obj.parent.datapath, 'icons', 'custom');
4737
+ if (pathParts.length === 1) {
4738
+ iconName = pathParts[0];
4739
+ diskPath = obj.path.join(iconsRoot, iconName);
4740
+ } else {
4741
+ ownerKey = pathParts[0];
4742
+ iconName = pathParts[1];
4743
+ diskPath = obj.path.join(iconsRoot, ownerKey, iconName);
4744
+ const currentUserKey = getCustomIconUserKey(user);
4745
+ isOwned = (currentUserKey != null) && (ownerKey === currentUserKey);
4746
+ }
4747
+
4748
+ const lower = iconName.toLowerCase();
4749
+ if ((lower.endsWith('.svg') === false) && (lower.endsWith('.png') === false)) { return null; }
4750
+ return { ownerKey: ownerKey, iconName: iconName, diskPath: diskPath, isOwned: isOwned, isLegacy: (pathParts.length === 1) };
4751
+ }
4752
+
4753
+ function handleCustomIconDelete(req, res) {
4754
+ const domain = checkUserIpAddress(req, res);
4755
+ if (domain == null) { return; }
4756
+ if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; }
4757
+ const user = obj.users[req.session.userid];
4758
+ if (user == null) { res.sendStatus(401); return; }
4759
+
4760
+ const iconPath = (req.body && (typeof req.body.iconPath === 'string')) ? req.body.iconPath : null;
4761
+ const iconInfo = resolveCustomIconPath(iconPath, user);
4762
+ if ((iconInfo == null) || (iconInfo.isOwned !== true)) { res.status(400).json({ success: false, error: 'Invalid icon path.' }); return; }
4763
+
4764
+ obj.fs.unlink(iconInfo.diskPath, function (err) {
4765
+ if (err && (err.code !== 'ENOENT')) { res.status(500).json({ success: false, error: 'Failed to delete icon.' }); return; }
4766
+ res.json({ success: true });
4767
+ });
4768
+ }
4769
+
4770
+ function handleCustomIconDownload(req, res) {
4771
+ const domain = getDomain(req);
4772
+ if (domain == null) { res.sendStatus(404); return; }
4773
+ if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; }
4774
+ const user = obj.users[req.session.userid];
4775
+ if (user == null) { res.sendStatus(401); return; }
4776
+
4777
+ if ((req.params == null) || (typeof req.params[0] !== 'string')) { res.sendStatus(404); return; }
4778
+ const iconInfo = resolveCustomIconPath('/icons/custom/' + req.params[0], user);
4779
+ if (iconInfo == null) { res.sendStatus(404); return; }
4780
+ if ((iconInfo.isLegacy !== true) && (iconInfo.isOwned !== true)) { res.sendStatus(404); return; }
4781
+ const iconNameLower = iconInfo.iconName.toLowerCase();
4782
+
4783
+ obj.fs.readFile(iconInfo.diskPath, function (err, data) {
4784
+ if (err) { res.sendStatus(404); return; }
4785
+ res.set({ 'Content-Type': iconNameLower.endsWith('.png') ? 'image/png' : 'image/svg+xml' });
4786
+ res.send(data);
4787
+ });
4788
+ }
4789
+
4790
function handleUploadFile(req, res) {
4791
const domain = checkUserIpAddress(req, res);
4792
if (domain == null) { return; }
4828
var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
4829
if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
4830
for (var i = 0; i < names.length; i++) {
4649
- if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
4831
+ var originalName = names[i];
4832
+ var safeName = obj.path.basename(originalName);
4833
+ if ((safeName !== originalName) || (obj.common.IsFilenameValid(safeName) == false)) { res.sendStatus(404); return; }
4834
var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
4835
if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
4836
// Create the user folder if needed
4841
obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4842
});
4843
});
4660
- })(xfile.fullpath, names[i], filedata);
4844
+ })(xfile.fullpath, safeName, filedata);
4845
} else {
4846
// Send a notification
4847
obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
4850
}
4851
} else {
4852
// More typical upload method, the file data is in a multipart mime post.
4669
- files.files.forEach(function (file) {
4670
- var fpath = obj.path.join(xfile.fullpath, file.originalFilename);
4671
- if (obj.common.IsFilenameValid(file.originalFilename) && ((xfile.quota == null) || ((totalsize + file.size) < xfile.quota))) { // Check if quota would not be broken if we add this file
4853
+ for (var i in files.files) {
4854
+ var file = files.files[i];
4855
+ var originalFilename = (typeof file.originalFilename === 'string') ? file.originalFilename : '';
4856
+ var safeOriginalFilename = obj.path.basename(originalFilename);
4857
+ var isFilenameAcceptable = (safeOriginalFilename === originalFilename) && obj.common.IsFilenameValid(safeOriginalFilename);
4858
+ const uploadTempPath = resolveSafeUploadTempPath(file.path);
4859
+ if (uploadTempPath == null) { res.sendStatus(400); return; }
4860
+ if (isFilenameAcceptable && ((xfile.quota == null) || ((totalsize + file.size) < xfile.quota))) { // Check if quota would not be broken if we add this file
4861
+ var fpath = obj.path.join(xfile.fullpath, safeOriginalFilename);
4862
4863
// See if we need to create the folder
4864
var domainx = 'domain';
4868
try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
4869
4870
// Rename the file
4681
- obj.fs.rename(file.path, fpath, function (err) {
4871
+ obj.fs.rename(uploadTempPath, fpath, function (err) {
4872
if (err && (err.code === 'EXDEV')) {
4873
// On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4684
- obj.common.copyFile(file.path, fpath, function (err) {
4685
- obj.fs.unlink(file.path, function (err) {
4874
+ obj.common.copyFile(uploadTempPath, fpath, function (err) {
4875
+ obj.fs.unlink(uploadTempPath, function (err) {
4876
obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4877
});
4878
});
4883
} else {
4884
// Send a notification
4885
obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
4696
- try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
4886
+ try { obj.fs.unlink(uploadTempPath, function (err) { }); } catch (e) { }
4887
}
4698
- });
4888
+ }
4889
}
4890
} else {
4891
// Send a notification
4937
try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
4938
4939
// More typical upload method, the file data is in a multipart mime post.
4750
- files.files.forEach(function (file) {
4751
- var ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
4940
+ for (var i in files.files) {
4941
+ var file = files.files[i];
4942
+ const ftarget = getRandomPassword() + '-' + file.originalFilename;
4943
+ const targetPath = obj.path.join(serverpath, ftarget);
4944
+ const uploadTempPath = resolveSafeUploadTempPath(file.path);
4945
+ if (uploadTempPath == null) { res.sendStatus(400); return; }
4946
cmd.files.push({ name: file.originalFilename, target: ftarget });
4947
// Rename the file
4754
- obj.fs.rename(file.path, fpath, function (err) {
4948
+ obj.fs.rename(uploadTempPath, targetPath, function (err) {
4949
if (err && (err.code === 'EXDEV')) {
4950
// On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4757
- obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
4951
+ obj.common.copyFile(uploadTempPath, targetPath, function (err) { obj.fs.unlink(uploadTempPath, function (err) { }); });
4952
}
4953
});
4760
- });
4954
+ }
4955
4956
// Instruct one of more agents to download a URL to a given local drive location.
4957
var tlsCertHash = null;
5288
}
5289
5290
// Close the recording file
5097
- if (ws.logfile != null) {
5291
+ if (ws.logfile != null) {
5292
setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5099
- obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5293
+ obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5294
obj.fs.close(logfile.fd);
5295
parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5296
// Compute session length
5342
// Close the recording file
5343
if (ws.logfile != null) {
5344
setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5151
- obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5345
+ obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5346
obj.fs.close(logfile.fd);
5347
parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5348
// Compute session length
5418
// Close the recording file
5419
if (ws.logfile != null) {
5420
setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5227
- obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5421
+ obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5422
obj.fs.close(logfile.fd);
5423
parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5424
// Compute session length
5460
// Close the recording file
5461
if (ws.logfile != null) {
5462
setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5269
- obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5463
+ obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5464
obj.fs.close(logfile.fd);
5465
parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5466
// Compute session length
5563
if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
5564
var msg = 'Started relay session', msgid = 13, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
5565
var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip, protocol: 101, nodeid: node._id };
5372
- obj.parent.DispatchEvent(['*', user._id], obj, event);
5566
+ obj.parent.DispatchEvent(['*', user._id], obj, event);
5567
}
5568
5569
// Update user last access time
5876
if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
5877
5878
// Require modules
5685
- const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
5879
+ const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
5880
5881
// Good practice to catch this error explicitly
5882
archive.on('error', function (err) { throw err; });
5884
// Set the archive name
5885
res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
5886
5693
- // Pipe archive data to the file
5887
+ // Pipe archive data to the file
5888
archive.pipe(res);
5889
5890
// Append files from a glob pattern
5891
archive.directory(obj.parent.datapath, false);
5892
5699
- // Finalize the archive (ie we are done appending files but streams have to finish yet)
5893
+ // Finalize the archive (ie we are done appending files but streams have to finish yet)
5894
archive.finalize();
5895
}
5896
7039
var selfurl = ' wss://' + req.headers.host;
7040
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
7041
const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
7042
+ const extraImgSrc = (parent.config.settings.extraimgsrc != null) ? (' ' + parent.config.settings.extraimgsrc) : '';
7043
+ const allowedFramingOriginsValue = (domain.allowedframingorigins != null) ? domain.allowedframingorigins : parent.config.settings.allowedframingorigins;
7044
+ const hasAllowedFramingOrigins = (allowedFramingOriginsValue != null);
7045
+ var framingOrigins = [];
7046
+ if (typeof allowedFramingOriginsValue === 'string') {
7047
+ framingOrigins = allowedFramingOriginsValue.split(/[,\s]+/).map(function (v) { return v.trim(); }).filter(function (v) { return v.length > 0; });
7048
+ } else if (Array.isArray(allowedFramingOriginsValue)) {
7049
+ framingOrigins = allowedFramingOriginsValue.filter(function (v) { return (typeof v === 'string') && (v.trim().length > 0); }).map(function (v) { return v.trim(); });
7050
+ }
7051
7052
// If the web relay port is enabled, allow the web page to redirect to it
7053
var extraFrameSrc = '';
7055
extraFrameSrc = ' https://' + req.headers.host + ':' + parent.webrelayserver.port;
7056
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
7057
}
6855
-
7058
+
7059
7060
// If using duo add apihostname to CSP
7061
var duoSrc = '';
7063
duoSrc = domain.duo2factor.apihostname;
7064
}
7065
6863
- // If a custom OIDC button icon URL is configured, allow its origin in img-src CSP
6864
- var extraImgSrc = '';
6865
- if (obj.common.validateObject(domain.authstrategies) && obj.common.validateObject(domain.authstrategies.oidc) && obj.common.validateObject(domain.authstrategies.oidc.custom)) {
6866
- const seen = {};
6867
- const urls = [domain.authstrategies.oidc.custom.buttoniconurl, domain.authstrategies.oidc.custom.buttoniconurl2x];
6868
- for (var k = 0; k < urls.length; k++) {
6869
- if (obj.common.validateUrl(urls[k])) {
6870
- try { const u = new URL(urls[k]); if (!seen[u.origin]) { extraImgSrc += ' ' + u.origin; seen[u.origin] = true; } } catch (e) {}
6871
- }
6872
- }
6873
- }
6874
-
6875
- // allowedFramingOrigins: domain override, else settings
6876
- var allowedFramingOriginsVal = (domain != null && domain.allowedframingorigins != null) ? domain.allowedframingorigins : parent.config.settings.allowedframingorigins;
6877
- var framingOrigins = parseAllowedFramingOrigins(allowedFramingOriginsVal);
6878
- var hasAllowedFramingOrigins = (domain != null && domain.allowedframingorigins != null) || (parent.config.settings.allowedframingorigins != null);
6879
-
6880
-
7066
// Finish setup security headers
7067
var cspBase = "default-src 'none'; font-src 'self' fonts.gstatic.com data:; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' " + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + extraImgSrc + " data:; style-src 'self' 'unsafe-inline' fonts.googleapis.com; frame-src 'self' blob: mcrouter:" + extraFrameSrc + "; media-src 'self'; form-action 'self' " + duoSrc + "; manifest-src 'self'";
7068
if (hasAllowedFramingOrigins) {
7228
obj.app.get(url + 'commander.ashx', handleMeshCommander);
7229
obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
7230
obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
7231
+ obj.app.post(url + 'customiconupload.ashx', handleCustomIconUpload);
7232
+ obj.app.post(url + 'customicondelete.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCustomIconDelete);
7233
+ obj.app.get(url + 'icons/custom/*', handleCustomIconDownload);
7234
obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
7235
obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
7236
obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
7305
}
7306
obj.app.get(url + 'invite', handleInviteRequest);
7307
obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
7120
-
7308
+
7309
if (parent.pluginHandler != null) {
7310
obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
7311
obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
7630
// Notify account 2fa failed login
7631
const ua = obj.getUserAgentInfo(req);
7632
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { action: 'authfail', username: user.name, userid: user._id, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + req.clientIp, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
7445
- obj.setbad2Fa(req);
7633
+ obj.setbad2Fa(req);
7634
res.redirect(domain.url + getQueryPortion(req));
7635
});
7636
} else {
7988
parent.debug('web', '404 Error ' + req.url);
7989
var domain = getDomain(req);
7990
if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
7803
- if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
7991
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
7992
const cspNonce = obj.crypto.randomBytes(15).toString('base64');
7993
res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
7994
res.status(404).render(getRenderPage((domain.sitestyle >= 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
8010
parent.debug('web', '404 Error ' + req.url);
8011
var domain = getDomain(req);
8012
if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
7825
- if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
8013
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
8014
if (obj.args.nice404 == false) { res.sendStatus(404); return; }
8015
const cspNonce = obj.crypto.randomBytes(15).toString('base64');
8016
res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
8551
if (Buffer.isBuffer(data[0])) {
8552
data = Buffer.concat(data);
8553
data = data.toString();
8366
- } else { // else if (typeof data[0] == 'string')
8554
+ } else { // else if (typeof data[0] == 'string')
8555
data = data.join();
8556
}
8557
} catch (err) {
8606
return authStrategyFlags;
8607
}
8608
8421
- // Handle an incoming request as a web relay
8609
+ // Handle an incoming request as a web relay
8610
function handleWebRelayRequest(req, res) {
8611
var webRelaySessionId = null;
8612
if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
8626
}
8627
}
8628
8441
- // Handle an incoming websocket connection as a web relay
8629
+ // Handle an incoming websocket connection as a web relay
8630
function handleWebRelayWebSocket(ws, req) {
8631
var webRelaySessionId = null;
8632
if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
9070
if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
9071
parent.debug('web', 'Invalid login, asking for email validation');
9072
try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { }
8885
- } else {
9073
+ } else {
9074
req.session.userid = user._id;
9075
req.session.ip = req.clientIp;
9076
setSessionRandom(req);
9985
xargs.title1 = domain.title1 ? domain.title1 : '';
9986
xargs.title2 = (domain.title1 && domain.title2) ? domain.title2 : '';
9987
}
9800
- xargs.title2 = obj.common.replacePlaceholders(xargs.title2, {
9988
+ xargs.title2 = obj.common.replacePlaceholders(xargs.title2, {
9989
'serverversion': obj.parent.currentVer,
9990
'servername': obj.getWebServerName(domain, req),
9991
'agentsessions': Object.keys(parent.webserver.wsagents).length,
10359
if (ua.browser && ua.browser.name) { ua.browserStr = ua.browser.name; if (ua.browser.version) { ua.browserStr += '/' + ua.browser.version } }
10360
if (ua.os && ua.os.name) { ua.osStr = ua.os.name; if (ua.os.version) { ua.osStr += '/' + ua.os.version } }
10361
// If the platform is set, use that instead of the OS
10174
- if (ua.platform) {
10362
+ if (ua.platform) {
10363
ua.osStr = ua.platform;
10364
// Special case for Windows 11
10365
if (ua.platformVersion) {
10373
} catch (ex) { return { browserStr: browser, osStr: os } }
10374
}
10375
10188
- // Return the query string portion of the URL, the ? and anything after BUT remove secret keys from authentication providers
10376
+ // Return the query string portion of the URL, the ? and anything after BUT remove secret keys from authentication providers
10377
function getQueryPortion(req) {
10190
- var removeKeys = ['duo_code', 'state']; // Keys to remove
10378
+ var removeKeys = ['duo_code', 'state']; // Keys to remove
10379
var s = req.url.indexOf('?');
10380
if (s == -1) {
10381
if (req.body && req.body.urlargs) {
10464
if (parent.config.settings.maxinvalidlogin === false) return true;
10465
if (typeof ip == 'object') { ip = ip.clientIp; }
10466
var splitip = ip.split('.');
10279
- if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
10467
+ if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
10468
var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
10469
var ipTable = obj.badLoginTable[ip];
10470
if (ipTable == null) return true;
10522
if (parent.config.settings.maxinvalid2fa === false) return true;
10523
if (typeof ip == 'object') { ip = ip.clientIp; }
10524
var splitip = ip.split('.');
10337
- if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
10525
+ if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
10526
var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
10527
var ipTable = obj.bad2faTable[ip];
10528
if (ipTable == null) return true;