Fixed content disposition crashes.
Ylian Saint-Hilaire committed
Oct 23, 2020 at 13:49 UTC
27193af0c5e7136e4093bdda85f41dc319e59eb0
2 files changed
+58
-54
meshdevicefile.js
+16
-14
@@ -203,20 +203,7 @@ module.exports.CreateMeshDeviceFile = function (parent, ws, res, req, domain, us
203
try { cmd = JSON.parse(data); } catch (ex) { }
204
if ((cmd == null) || (typeof cmd.op == 'string')) {
205
if (cmd.op == 'ok') {
206
- var filename = require('path').basename(this.file).split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
207
- if (typeof cmd.size == 'number') {
208
- try {
209
- this.res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="' + filename + '"', 'Content-Length': cmd.size });
210
- } catch (ex) {
211
- this.res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="file.bin"', 'Content-Length': cmd.size });
212
- }
213
- } else {
214
- try {
215
- this.res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="' + filename + '"' });
216
- } catch (ex) {
217
- this.res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="file.bin"'});
218
- }
219
- }
206
+ setContentDispositionHeader(this.res, 'application/octet-stream', filename, cmd.size, 'file.bin');
207
} else {
208
try { this.res.sendStatus(401); } catch (ex) { }
209
}
@@ -288,6 +275,21 @@ module.exports.CreateMeshDeviceFile = function (parent, ws, res, req, domain, us
275
if (obj.sendAgentMessage(command, user, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'FileRelay: Unable to contact this agent (' + obj.req.clientIp + ')'); }
276
}
277
278
+ // Set the content disposition header for a HTTP response.
279
+ // Because the filename can't have any special characters in it, we need to be extra careful.
280
+ function setContentDispositionHeader(res, type, name, size, altname) {
281
+ var name = require('path').basename(name).split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
282
+ try {
283
+ var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + name + '"' };
284
+ if (typeof size == 'number') { x['Content-Length'] = size; }
285
+ res.set(x);
286
+ } catch (ex) {
287
+ var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + altname + '"' };
288
+ if (typeof size == 'number') { x['Content-Length'] = size; }
289
+ res.set(x);
290
+ }
291
+ }
292
+
293
// If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session.
294
performRelay();
295
return obj;
webserver.js
+42
-40
@@ -2664,11 +2664,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2664
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2665
if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
2666
parent.debug('web', 'handleRootCertRequest()');
2667
- try {
2668
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="' + certificates.RootName + '.cer"' });
2669
- } catch (ex) {
2670
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="rootcert.cer"' });
2671
- }
2667
+ setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
2668
res.send(Buffer.from(getRootCertBase64(), 'base64'));
2669
}
2670
@@ -2689,11 +2685,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2685
try { stat = obj.fs.statSync(path); } catch (e) { }
2686
if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
2687
if (req.query.download == 1) {
2692
- try {
2693
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"' + filename + '\"' });
2694
- } catch (ex) {
2695
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"file.bin\"' });
2696
- }
2688
+ setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
2689
try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
2690
} else {
2691
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(), messageid: 1, fileurl: req.path + '?download=1', filename: filename, filesize: stat.size }, req, domain));
@@ -2908,7 +2900,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2900
if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
2901
2902
// Send the recorded file
2911
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"' + req.query.file + '\"' });
2903
+ setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
2904
try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
2905
}
2906
@@ -3003,11 +2995,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2995
if (user == null) { res.sendStatus(404); return; }
2996
const file = obj.getServerFilePath(user, domain, req.query.link);
2997
if (file == null) { res.sendStatus(404); return; }
3006
- try {
3007
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"' + file.name + '\"' });
3008
- } catch (ex) {
3009
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"file.bin\"' });
3010
- }
2998
+ setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
2999
obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
3000
}
3001
@@ -4031,7 +4019,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4019
for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
4020
var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
4021
4034
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="meshagent"' });
4022
+ setContentDispositionHeader(res, 'application/octet-stream', 'meshagent', null, 'meshagent');
4023
res.statusCode = 200;
4024
obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
4025
} else if (req.query.id != null) {
@@ -4039,7 +4027,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4027
var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
4028
if (argentInfo == null) { res.sendStatus(404); return; }
4029
if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
4042
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="' + argentInfo.rname + '"' });
4030
+ setContentDispositionHeader(res, 'application/octet-stream', argentInfo.rname, null, 'meshagent');
4031
if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.end(argentInfo.data); }
4032
} else {
4033
// Check if the meshid is a time limited, encrypted cookie
@@ -4085,11 +4073,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4073
if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
4074
if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
4075
4088
- try {
4089
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="' + meshfilename + '"' });
4090
- } catch (ex) {
4091
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="' + argentInfo.rname + '"' });
4092
- }
4076
+ setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, argentInfo.rname);
4077
obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: obj.parent.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: obj.parent.meshAgentBinaries[req.query.id].pe });
4078
}
4079
} else if (req.query.script != null) {
@@ -4098,7 +4082,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4082
// Send a specific mesh install script back
4083
var scriptInfo = obj.parent.meshAgentInstallScripts[req.query.script];
4084
if (scriptInfo == null) { res.sendStatus(404); return; }
4101
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="' + scriptInfo.rname + '"' });
4085
+ setContentDispositionHeader(res, 'application/octet-stream', scriptInfo.rname, null, 'script');
4086
var data = scriptInfo.data;
4087
var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
4088
if (obj.isTrustedCert(domain) != true) {
@@ -4122,17 +4106,23 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4106
if ((agentid == 3)) { // Signed Windows MeshCmd.exe x86
4107
var stats = null, meshCmdPath = obj.path.join(__dirname, 'agents', 'MeshCmd-signed.exe');
4108
try { stats = obj.fs.statSync(meshCmdPath); } catch (e) { }
4125
- if ((stats != null)) { res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="meshcmd' + ((req.query.meshcmd <= 3) ? '.exe' : '') + '"' }); res.sendFile(meshCmdPath); return; }
4109
+ if ((stats != null)) {
4110
+ setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 3) ? '.exe' : ''), null, 'meshcmd');
4111
+ res.sendFile(meshCmdPath); return;
4112
+ }
4113
} else if ((agentid == 4)) { // Signed Windows MeshCmd64.exe x64
4114
var stats = null, meshCmd64Path = obj.path.join(__dirname, 'agents', 'MeshCmd64-signed.exe');
4115
try { stats = obj.fs.statSync(meshCmd64Path); } catch (e) { }
4129
- if ((stats != null)) { res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : '') + '"' }); res.sendFile(meshCmd64Path); return; }
4116
+ if ((stats != null)) {
4117
+ setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
4118
+ res.sendFile(meshCmd64Path); return;
4119
+ }
4120
}
4121
// No signed agents, we are going to merge a new MeshCmd.
4122
if ((agentid < 10000) && (obj.parent.meshAgentBinaries[agentid + 10000] != null)) { agentid += 10000; } // Avoid merging javascript to a signed mesh agent.
4123
var argentInfo = obj.parent.meshAgentBinaries[agentid];
4124
if ((argentInfo == null) || (obj.parent.defaultMeshCmd == null)) { res.sendStatus(404); return; }
4135
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : '') + '"' });
4125
+ setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
4126
res.statusCode = 200;
4127
if (argentInfo.signedMeshCmdPath != null) {
4128
// If we have a pre-signed MeshCmd, send that.
@@ -4168,7 +4158,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4158
if (req.query.key != null) { meshaction.loginKey = req.query.key; }
4159
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4160
if (obj.args.lanonly != true) { meshaction.serverUrl = ((obj.args.notls == true) ? 'ws://' : 'wss://') + obj.getWebServerName(domain) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
4171
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="meshaction.txt"' });
4161
+
4162
+ setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
4163
res.send(JSON.stringify(meshaction, null, ' '));
4164
});
4165
} else if (req.query.meshaction == 'generic') {
@@ -4183,12 +4174,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4174
if (req.query.key != null) { meshaction.loginKey = req.query.key; }
4175
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4176
if (obj.args.lanonly != true) { meshaction.serverUrl = ((obj.args.notls == true) ? 'ws://' : 'wss://') + obj.getWebServerName(domain) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
4186
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="meshaction.txt"' });
4177
+ setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
4178
res.send(JSON.stringify(meshaction, null, ' '));
4179
} else if (req.query.meshaction == 'winrouter') {
4180
var p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe');
4181
if (obj.fs.existsSync(p)) {
4191
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="MeshCentralRouter.exe"' });
4182
+ setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
4183
try { res.sendFile(p); } catch (e) { res.sendStatus(404); }
4184
} else { res.sendStatus(404); }
4185
} else {
@@ -4211,7 +4202,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4202
// Download a dump file
4203
var dumpFile = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.dldump);
4204
if (obj.fs.existsSync(dumpFile)) {
4214
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="' + req.query.dldump + '' });
4205
+ setContentDispositionHeader(res, 'application/octet-stream', req.query.dldump, null, 'file.bin');
4206
res.sendFile(dumpFile); return;
4207
} else {
4208
res.sendStatus(404); return;
@@ -4349,13 +4340,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4340
// Setup the response output
4341
var archive = require('archiver')('zip', { level: 5 }); // Sets the compression method.
4342
archive.on('error', function (err) { throw err; });
4352
- try {
4353
- // Set the agent download including the mesh name.
4354
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="MeshAgent-' + mesh.name + '.zip"' });
4355
- } catch (ex) {
4356
- // If the mesh name contains invalid characters, just use a generic name.
4357
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="MeshAgent.zip"' });
4358
- }
4343
+
4344
+ // Set the agent download including the mesh name.
4345
+ setContentDispositionHeader(res, 'application/octet-stream', 'MeshAgent-' + mesh.name + '.zip', null, 'MeshAgent.zip');
4346
archive.pipe(res);
4347
4348
// Opens the "MeshAgentOSXPackager.zip"
@@ -4453,7 +4440,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4440
var meshsettings = getMshFromRequest(req, res, domain);
4441
if (meshsettings == null) { res.sendStatus(401); return; }
4442
4456
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="meshagent.msh"' });
4443
+ setContentDispositionHeader(res, 'application/octet-stream', 'meshagent.msh', null, 'meshagent.msh');
4444
res.send(meshsettings);
4445
};
4446
@@ -4477,7 +4464,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4464
if (obj.GetNodeRights(user, node.meshid, node._id) == 0) { res.sendStatus(401); return; }
4465
4466
// Get the list of power events and send them
4480
- res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename="powerevents.csv"' });
4467
+ setContentDispositionHeader(res, 'application/octet-stream', 'powerevents.csv', null, 'powerevents.csv');
4468
obj.db.getPowerTimeline(node._id, function (err, docs) {
4469
var xevents = ['Time, State, Previous State'], prevState = 0;
4470
for (var i in docs) {
@@ -6269,6 +6256,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
6256
// Clean a IPv6 address that encodes a IPv4 address
6257
function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
6258
6259
+ // Set the content disposition header for a HTTP response.
6260
+ // Because the filename can't have any special characters in it, we need to be extra careful.
6261
+ function setContentDispositionHeader(res, type, name, size, altname) {
6262
+ var name = require('path').basename(name).split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
6263
+ try {
6264
+ var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + name + '"' };
6265
+ if (typeof size == 'number') { x['Content-Length'] = size; }
6266
+ res.set(x);
6267
+ } catch (ex) {
6268
+ var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + altname + '"' };
6269
+ if (typeof size == 'number') { x['Content-Length'] = size; }
6270
+ res.set(x);
6271
+ }
6272
+ }
6273
+
6274
// Record a new entry in a recording log
6275
function recordingEntry(fd, type, flags, data, func, tag) {
6276
try {