- Small update for CGI Security
- Various AI suggested fixes
- Fix to logging verbosity
- Fixes for Last-Modified headers
- Removed 2 unused node module deps
- Added pc-only user_config.json example
- Initial work updating catchall system to support CGI
- Reverted registration and home theme system due to problems
This commit is contained in:
zefie
2025-01-03 12:50:29 -05:00
parent 907cec23c2
commit 65521fcad6
45 changed files with 421 additions and 716 deletions

View File

@@ -321,6 +321,14 @@ var runScriptInVM = function (script_data, user_contextObj = {}, privileged = fa
async function handleCGI(executable, cgi_file, socket, request_headers, vault, service_name, session_data = null, extra_path = "")
{
const SAFE_ROOT = path.resolve(__dirname);
vault = path.resolve(vault);
if (!vault.startsWith(SAFE_ROOT)) {
console.error("Invalid vault path:", vault);
var errpage = wtvshared.doErrorPage(403);
sendToClient(socket, errpage[0], errpage[1]);
return;
}
var env = wtvshared.cloneObj(process.env);
env.QUERY_STRING = "";
var request_data = new Array();
@@ -591,7 +599,7 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
})
if (request_is_async && !minisrv_config.config.debug_flags.quiet) console.debug(" * Script requested Asynchronous mode");
} else if (fs.existsSync(service_vault_file_path + ".php") || (service_vault_file_path.indexOf(".php") == service_vault_file_path.length - 4 && fs.existsSync(service_vault_file_path)) || fs.existsSync(service_vault_file_path + ".php")) {
} else if (fs.existsSync(service_vault_file_path + ".php") || (service_vault_file_path.endsWith(".php") && fs.existsSync(service_vault_file_path)) || service_vault_file_path.indexOf(".php") > 0) {
request_is_async = true;
if (minisrv_config.config.php_enabled && minisrv_config.config.php_binpath) {
if (fs.existsSync(service_vault_file_path + ".php") || fs.existsSync(service_vault_file_path)) {
@@ -618,7 +626,7 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
sendToClient(socket, errpage[0], errpage[1]);
return;
}
} else if (fs.existsSync(service_vault_file_path + ".cgi") || (service_vault_file_path.indexOf(".cgi") == service_vault_file_path.length - 4 && fs.existsSync(service_vault_file_path)) || fs.existsSync(service_vault_file_path + ".cgi")) {
} else if (fs.existsSync(service_vault_file_path + ".cgi") || (service_vault_file_path.endsWith(".cgi") && fs.existsSync(service_vault_file_path)) || service_vault_file_path.indexOf(".cgi") > 0) {
request_is_async = true;
if (minisrv_config.config.php_enabled && minisrv_config.config.php_binpath) {
if (fs.existsSync(service_vault_file_path + ".cgi") || fs.existsSync(service_vault_file_path)) {
@@ -730,11 +738,10 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
sendRawFile(socket, service_vault_file_path);
}
} else {
// look for a catchallin the current path and all parent paths up until the service root
if (minisrv_config.config.catchall_file_name) {
var minisrv_catchall_file_name = null;
if (minisrv_config.services[service_name]) minisrv_catchall_file_name = minisrv_config.services[service_name].catchall_file_name || minisrv_config.config.catchall_file_name || null;
else minisrv_catchall_file_name = minisrv_config.config.catchall_file_name || null;
// look for a catchall in the current path and all parent paths up until the service root
var service_config = minisrv_config.services[service_name] || {};
if (minisrv_config.config.catchall_file_name || service_config['catchall_file_name']) {
var minisrv_catchall_file_name = service_config['catchall_file_name'] || minisrv_config.config.catchall_file_name || null;
if (minisrv_catchall_file_name) {
var service_check_dir = service_vault_file_path.split(path.sep);
service_check_dir.pop(); // pop filename
@@ -742,21 +749,48 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
while (service_check_dir.join(path.sep) != service_vault_dir && service_check_dir.length > 0) {
var catchall_file = service_check_dir.join(path.sep) + path.sep + minisrv_catchall_file_name;
if (fs.existsSync(catchall_file)) {
service_vault_found = true;
if (!minisrv_config.config.debug_flags.quiet) console.debug(" * Found catchall at " + catchall_file + " to handle request (JS Interpreter Mode) [Socket " + socket.id + "]");
request_headers.service_file_path = catchall_file;
var script_data = fs.readFileSync(catchall_file).toString();
if (catchall_file.endsWith(".js")) {
var script_data = fs.readFileSync(catchall_file).toString();
var vmResults = runScriptInVM(script_data, contextObj, privileged, catchall_file);
var vmResults = runScriptInVM(script_data, contextObj, privileged, catchall_file);
updateFromVM.forEach((item) => {
// Here we read back certain data from the ServiceVault Script Context Object
try {
if (typeof vmResults[item[1]] !== "undefined") eval(item[0] + ' = vmResults["' + item[1] + '"]');
} catch (e) {
console.error("vm readback error", e);
updateFromVM.forEach((item) => {
// Here we read back certain data from the ServiceVault Script Context Object
try {
if (typeof vmResults[item[1]] !== "undefined") eval(item[0] + ' = vmResults["' + item[1] + '"]');
} catch (e) {
console.error("vm readback error", e);
}
});
} else if (catchall_file.endsWith(".php")) {
if (minisrv_config.config.php_enabled && minisrv_config.config.php_binpath) {
request_is_async = true;
var extra_path = service_check_dir.join(path.sep) + path.sep + request_headers.request_url.replace(service_name + ":/", "");
if (!minisrv_config.config.debug_flags.quiet) console.debug(" * Found catchall at " + catchall_file + " to handle request (CGI Interpreter Mode) [Socket " + socket.id + "]");
handlePHP(socket, request_headers, catchall_file, service_vault_dir + path.sep + service_name, (pc_services) ? pc_service_name : service_name, (pc_services) ? null : ssid_sessions[socket.ssid], extra_path)
} else {
// php is not enabled, don't expose source code
var errpage = wtvshared.doErrorPage(403);
sendToClient(socket, errpage[0], errpage[1]);
return;
}
});
} else if (catchall_file.endsWith(".cgi")) {
if (minisrv_config.config.cgi_enabled) {
request_is_async = true;
var extra_path = service_check_dir.join(path.sep) + path.sep + request_headers.request_url.replace(service_name + ":/", "");
if (!minisrv_config.config.debug_flags.quiet) console.debug(" * Found catchall at " + catchall_file + " to handle request (CGI Interpreter Mode) [Socket " + socket.id + "]");
handleCGI(catchall_file, catchall_file, socket, request_headers, service_vault_dir + path.sep + service_name, (pc_services) ? pc_service_name : service_name, (pc_services) ? null : ssid_sessions[socket.ssid], extra_path)
} else {
// cgi is not enabled, don't expose source code
var errpage = wtvshared.doErrorPage(403);
sendToClient(socket, errpage[0], errpage[1]);
return;
}
}
if (request_is_async && !minisrv_config.config.debug_flags.quiet) console.debug(" * Script requested Asynchronous mode");
break;
@@ -1333,7 +1367,7 @@ function headerStringToObj(headers, response = false) {
async function sendToClient(socket, headers_obj, data = null) {
var headers = "";
var content_length = 0;
var end_of_line = "\n";
var eol = "\n";
if (typeof (data) === 'undefined' || data === null) data = '';
if (typeof (headers_obj) === 'string') {
// string to header object
@@ -1398,16 +1432,23 @@ async function sendToClient(socket, headers_obj, data = null) {
if (socket_sessions[socket.id]) {
if (socket_sessions[socket.id].request_headers) {
if (socket_sessions[socket.id].request_headers.service_file_path) {
if (wtvshared.getFileExt(socket_sessions[socket.id].request_headers.service_file_path).toLowerCase() !== "js" || socket_sessions[socket.id].request_headers.raw_file === true) {
var last_modified = wtvshared.getFileLastModifiedUTCString(socket_sessions[socket.id].request_headers.service_file_path);
if (last_modified) headers_obj["Last-Modified"] = last_modified;
// Don't change Last-modified header if provided already
if (!headers['Last-Modified']) {
// Only add the header if not a js, php, or cgi file
if (wtvshared.getFileExt(socket_sessions[socket.id].request_headers.service_file_path).toLowerCase() !== "js" ||
wtvshared.getFileExt(socket_sessions[socket.id].request_headers.service_file_path).toLowerCase() !== "php" ||
wtvshared.getFileExt(socket_sessions[socket.id].request_headers.service_file_path).toLowerCase() !== "cgi" ||
socket_sessions[socket.id].request_headers.raw_file === true) {
var last_modified = wtvshared.getFileLastModifiedUTCString(socket_sessions[socket.id].request_headers.service_file_path);
if (last_modified) headers_obj["Last-Modified"] = last_modified;
}
}
}
}
}
// if box can do compression, see if its worth enabling
// if client can do compression, see if its worth enabling
// small files actually get larger, so don't compress them
var compression_type = 0;
if (content_length >= 256) compression_type = wtvmime.shouldWeCompress(ssid_sessions[socket.ssid], headers_obj);
@@ -1428,7 +1469,7 @@ async function sendToClient(socket, headers_obj, data = null) {
}
if (socket.res) { // pc mode with response object available
if (compression_type == 1) compression_type = 2; // just in case
if (compression_type == 1) compression_type = 2; // wtv-lzpf not supported in pc mode
}
// compress if needed
@@ -1497,7 +1538,6 @@ async function sendToClient(socket, headers_obj, data = null) {
if (headers_obj["minisrv-force-content-length"]) {
headers_obj["Content-length"] = headers_obj["minisrv-force-content-length"];
delete headers_obj["minisrv-force-content-length"];
}
if (!socket.res) {
@@ -1525,25 +1565,27 @@ async function sendToClient(socket, headers_obj, data = null) {
var xpower = wtvshared.getCaseInsensitiveKey("x-powered-by", headers_obj);
if (!xpower) {
// add X-Powered-By header if not WebTV and not already set
if (!socket.ssid) headers_obj['X-Powered-By'] = "NodeJS ("+process.version+") Express via " + z_title;
xpower = 'X-Powered-By';
if (!socket.ssid) headers_obj[xpower] = "NodeJS ("+process.version+") Express via " + z_title;
} else {
// delete if webtv
if (socket.ssid) delete headers_obj[xpower];
}
headers_obj = wtvshared.moveObjectKey("x-powered-by", -2, headers_obj, true) // move x-powered-by before Content-type
if (headers_obj[xpower]) headers_obj = wtvshared.moveObjectKey(xpower, -2, headers_obj, true) // move x-powered-by before Content-type
if (!socket.res) {
// header object to string
if (minisrv_config.config.debug_flags.show_headers) console.debug(" * Outgoing headers on socket ID", socket.id, headers_obj);
Object.keys(headers_obj).forEach(function (k) {
if (k == "Status") {
headers += headers_obj[k] + end_of_line;
headers += headers_obj[k] + eol;
} else {
if (k.indexOf('_') >= 0) {
var j = k.split('_')[0];
headers += j + ": " + headers_obj[k] + end_of_line;
headers += j + ": " + headers_obj[k] + eol;
} else {
headers += k + ": " + headers_obj[k] + end_of_line;
headers += k + ": " + headers_obj[k] + eol;
}
}
});
@@ -1554,29 +1596,37 @@ async function sendToClient(socket, headers_obj, data = null) {
}
}
}
// Delete any other stray minisrv headers (we process them all before this)
Object.keys(headers_obj).forEach(function (k) {
if (k.indexOf("minisrv-") == 0) {
delete headers_obj[k];
}
});
// send to client
if (socket.res) {
var resCode = parseInt(headers_obj.Status.substr(0, 3));
socket.res.writeHead(resCode, headers_obj);
socket.res.end(data);
if (minisrv_config.config.debug_flags.show_headers) console.debug(" * Outgoing PC headers on " + socket.service_name + " socket ID", socket.id, headers_obj);
if (minisrv_config.config.debug_flags.quiet) console.debug(" * Sent response " + headers_obj.status + " to PC client (Content-Type:", headers_obj['Content-Type'], "~", headers_obj['Content-length'], "bytes)");
if (minisrv_config.config.debug_flags.quiet) console.debug(" * Sent response " + headers_obj.Status + " to PC client (Content-Type:", headers_obj['Content-type'], "~", headers_obj['Content-length'], "bytes)");
} else {
var toClient = null;
if (typeof data == 'string') {
toClient = headers + end_of_line + data;
toClient = headers + eol + data;
sendToSocket(socket, Buffer.from(toClient));
} else if (typeof data == 'object') {
if (minisrv_config.config.debug_flags.quiet) var verbosity_mod = (headers_obj["wtv-encrypted"] == 'true') ? " encrypted response" : "";
if (socket_sessions[socket.id].secure_headers == true) {
// encrypt headers
if (minisrv_config.config.debug_flags.quiet) verbosity_mod += " with encrypted headers";
var enc_headers = socket_sessions[socket.id].wtvsec.Encrypt(1, headers + end_of_line);
var enc_headers = socket_sessions[socket.id].wtvsec.Encrypt(1, headers + eol);
sendToSocket(socket, new Buffer.from(concatArrayBuffer(enc_headers, data)));
} else {
sendToSocket(socket, new Buffer.from(concatArrayBuffer(Buffer.from(headers + end_of_line), data)));
sendToSocket(socket, new Buffer.from(concatArrayBuffer(Buffer.from(headers + eol), data)));
}
if (minisrv_config.config.debug_flags.quiet) console.debug(" * Sent" + verbosity_mod + " " + headers_obj.status + " to client (Content-Type:", headers_obj['Content-Type'], "~", headers_obj['Content-length'], "bytes)");
if (minisrv_config.config.debug_flags.quiet) console.debug(" * Sent" + verbosity_mod + " " + headers_obj.Status + " to client (Content-Type:", headers_obj['Content-type'], "~", headers_obj['Content-length'], "bytes)");
}
}
}
@@ -2425,11 +2475,17 @@ Content-type: text/html`;
});
request_headers.query = req.query;
if (req.body) {
var data = "";
for (var i=0; i<req.body.length; i++) {
data += String.fromCharCode(req.body[i]);
if (typeof(req.body) == "string") {
request_headers.post_data = req.body;
} else if (req.body.length) {
var data = "";
for (var i=0; i<req.body.length; i++) {
data += String.fromCharCode(req.body[i]);
}
request_headers.post_data = data;
} else {
request_headers.post_data = "";
}
request_headers.post_data = data;
} else {
request_headers.post_data = "";
}