Improved HTTPS file server in MPS.

Ylian Saint-Hilaire committed Jul 27, 2020 at 00:11 UTC 3bb9975a9ad44462656899013aa5b4568f0541f7
1 file changed +89 -4
mpsserver.js
+89 -4
@@ -16,6 +16,8 @@
16 // Construct a Intel AMT MPS server object
17 module.exports.CreateMpsServer = function (parent, db, args, certificates) {
18 var obj = {};
19 + obj.fs = require('fs');
20 + obj.path = require('path');
21 obj.parent = parent;
22 obj.db = db;
23 obj.args = args;
@@ -29,6 +31,12 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
31 const tls = require('tls');
32 const MAX_IDLE = 90000; // 90 seconds max idle time, higher than the typical KEEP-ALIVE periode of 60 seconds
33
34 + // This MPS server is also a tiny HTTPS server. HTTP responses are here.
35 + obj.httpResponses = {
36 + '/': '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body>MeshCentral MPS server.<br />Intel&reg; AMT computers should connect here.</body></html>'
37 + //'/text.ico': { file: 'c:\\temp\\test.iso', maxserve: 3, maxtime: Date.now() + 15000 }
38 + };
39 +
40 if (obj.args.mpstlsoffload) {
41 obj.server = net.createServer(onConnection);
42 } else {
@@ -223,9 +231,18 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
231
232 // Detect if this is an HTTPS request, if it is, return a simple answer and disconnect. This is useful for debugging access to the MPS port.
233 if (socket.tag.first == true) {
226 - if (socket.tag.accumulator.length < 3) return;
234 + if (socket.tag.accumulator.length < 5) return;
235 //if (!socket.tag.clientCert.subject) { console.log("MPS Connection, no client cert: " + socket.remoteAddress); socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nMeshCentral2 MPS server.\r\nNo client certificate given.'); socket.end(); return; }
228 - if (socket.tag.accumulator.substring(0, 3) == 'GET') { if (args.mpsdebug) { console.log("MPS Connection, HTTP GET detected: " + socket.remoteAddress); } socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body>MeshCentral2 MPS server.<br />Intel&reg; AMT computers should connect here.</body></html>"); socket.end(); return; }
236 + if ((socket.tag.accumulator.substring(0, 4) == 'GET ') || (socket.tag.accumulator.substring(0, 5) == 'HEAD ')) {
237 + if (args.mpsdebug) { console.log("MPS Connection, HTTP request detected: " + socket.remoteAddress); }
238 + socket.removeAllListeners('data');
239 + socket.removeAllListeners('close');
240 + socket.on('data', onHttpData);
241 + socket.on('close', onHttpClose);
242 + obj.httpSocket = socket;
243 + onHttpData.call(socket, data);
244 + return;
245 + }
246
247 // If the MQTT broker is active, look for inbound MQTT connections
248 if (parent.mqttbroker != null) {
@@ -238,8 +255,8 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
255 if (chunk.readUInt8(0) == 16 && ((chunk.slice(4, 8).toString() === 'MQTT') || (chunk.slice(5, 9).toString() === 'MQTT')
256 || (chunk.slice(6, 10).toString() === 'MQTT') || (chunk.slice(7, 11).toString() === 'MQTT'))) {
257 parent.debug('mps', "MQTT connection detected.");
241 - socket.removeAllListeners("data");
242 - socket.removeAllListeners("close");
258 + socket.removeAllListeners('data');
259 + socket.removeAllListeners('close');
260 socket.setNoDelay(true);
261 socket.serialtunnel = SerialTunnel();
262 socket.serialtunnel.xtransport = 'mps';
@@ -934,7 +951,75 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
951 if ((socket != null) && (socket.tag != null)) { socket.tag.meshid = newMeshId; }
952 }
953
954 + // Called when handling incoming HTTP data
955 + function onHttpData(data) {
956 + if (this.xdata == null) { this.xdata = data; } else { this.xdata += data; }
957 + var headersize = this.xdata.indexOf('\r\n\r\n');
958 + if (headersize < 0) { if (this.xdata.length > 4096) { this.end(); } return; }
959 + var headers = this.xdata.substring(0, headersize).split('\r\n');
960 + if (headers.length < 1) { this.end(); return; }
961 + var headerObj = {};
962 + for (var i = 1; i < headers.length; i++) { var j = headers[i].indexOf(': '); if (i > 0) { headerObj[headers[i].substring(0, j).toLowerCase()] = headers[i].substring(j + 2); } }
963 + var hostHeader = (headerObj['host'] != null) ? ('Host: ' + headerObj['host'] + '\r\n') : '';
964 + var directives = headers[0].split(' ');
965 + if ((directives.length != 3) || ((directives[0] != 'GET') && (directives[0] != 'HEAD'))) { this.end(); return; }
966 + //console.log('WebServer, request', directives[0], directives[1]);
967 + var responseCode = 404, responseType = 'application/octet-stream', responseData = '', r = null;
968 + if (obj.httpResponses != null) { r = obj.httpResponses[directives[1]]; }
969 + if ((r != null) && (r.maxtime != null) && (r.maxtime < Date.now())) { r = null; delete obj.httpResponses[directives[1]]; } // Check if this entry is expired.
970 + if (r != null) {
971 + if (typeof r == 'string') {
972 + responseCode = 200; responseType = 'text/html'; responseData = r;
973 + } else if (typeof r == 'object') {
974 + responseCode = 200;
975 + if (r.type) { responseType = r.type; }
976 + if (r.data) { responseData = r.data; }
977 + if (r.shortfile) { try { responseData = obj.fs.readFileSync(r.shortfile); } catch (ex) { responseCode = 404; responseType = 'text/html'; responseData = 'File not found'; } }
978 + if (r.file) {
979 + // Send the file header and pipe the rest of the file
980 + var filestats = null;
981 + try { filestats = obj.fs.statSync(r.file); } catch (ex) { }
982 + if ((filestats == null) || (typeof filestats.size != 'number') || (filestats.size <= 0)) {
983 + responseCode = 404; responseType = 'text/html'; responseData = 'File not found';
984 + } else {
985 + this.write('HTTP/1.1 200 OK\r\n' + hostHeader + 'Content-Type: ' + responseType + '\r\nConnection: keep-alive\r\nContent-Length: ' + filestats.size + '\r\n\r\n');
986 + if (directives[0] == 'GET') {
987 + obj.fs.createReadStream(r.file, { flags: 'r' }).pipe(this);
988 + if (typeof r.maxserve == 'number') { r.maxserve--; if (r.maxserve == 0) { delete obj.httpResponses[directives[1]]; } } // Check if this entry was server the maximum amount of times.
989 + }
990 + delete this.xdata;
991 + return;
992 + }
993 + }
994 + }
995 + } else {
996 + responseType = 'text/html';
997 + responseData = 'Invalid request';
998 + }
999 + this.write('HTTP/1.1 ' + responseCode + ' OK\r\n' + hostHeader + 'Connection: keep-alive\r\nContent-Type: ' + responseType + '\r\nContent-Length: ' + responseData.length + '\r\n\r\n');
1000 + this.write(responseData);
1001 + delete this.xdata;
1002 + }
1003 +
1004 + // Called when handling HTTP data and the socket closes
1005 + function onHttpClose() { }
1006 +
1007 + // Add a HTTP file response
1008 + obj.addHttpFileResponse = function (path, file, maxserve, minutes) {
1009 + var r = { file: file };
1010 + if (typeof maxserve == 'number') { r.maxserve = maxserve; }
1011 + if (typeof minutes == 'number') { r.maxtime = Date.now() + (60000 * minutes); }
1012 + obj.httpResponses[path] = r;
1013 +
1014 + // Clean up any expired files
1015 + const now = Date.now();
1016 + for (var i in obj.httpResponses) { if ((obj.httpResponses[i].maxtime != null) && (obj.httpResponses[i].maxtime < now)) { delete obj.httpResponses[i]; } }
1017 + }
1018 +
1019 function guidToStr(g) { return g.substring(6, 8) + g.substring(4, 6) + g.substring(2, 4) + g.substring(0, 2) + "-" + g.substring(10, 12) + g.substring(8, 10) + "-" + g.substring(14, 16) + g.substring(12, 14) + "-" + g.substring(16, 20) + "-" + g.substring(20); }
1020
1021 + // Example, this will add a file to stream, served 2 times max and 3 minutes max.
1022 + //obj.addHttpFileResponse('/a.png', 'c:\\temp\\MC2-LetsEncrypt.png', 2, 3);
1023 +
1024 return obj;
1025 };