svrjs-statistics-server/backend/serverSideScript.js

374 lines
14 KiB
JavaScript
Raw Permalink Normal View History

2024-08-06 14:49:57 +02:00
disableEndElseCallbackExecute = true; //Without "var", else it will not work!!!
var mysql = require("mysql");
var gnuplot = require("gnuplot"); //There is an OS command injection vulnerability in the "gnuplot" npm package, but since the statistics display part of the application doesn't involve user input, the application isn't affected by it.
if (!customvar1 && !customvar2) {
try {
customvar1 = JSON.parse(fs.readFileSync(__dirname + "/../dbconfig.json"));
} catch (err) {
customvar2 = err;
}
}
if (customvar2) {
// customvar2 is a instance of Error
callServerError(500, customvar2);
return;
}
// customvar1 is a database configuration
var connection = mysql.createConnection(customvar1);
function plot(data) {
var dataToFeed = [];
Object.keys(data).sort().forEach(function (key) {
var feedKey = key.replace(/"/g, "'");
if (feedKey.length > 32) feedKey = feedKey.substring(0, 32) + "...";
dataToFeed.push(dataToFeed.length + " \"" + feedKey + "\" " + parseFloat(data[key]));
2024-08-06 14:49:57 +02:00
});
if (dataToFeed.length == 0) dataToFeed.push("0 \"\" 0");
var gnuplotObject = gnuplot().set("terminal png size 800,480").set("tics font \"Poppins,12\"").set("xtics rotate by 45 right").set("rmargin 3").set("boxwidth 0.6").set("style fill solid").set("yrange [0:*]").set("grid ytics mytics").set("grid").plot("'-' using 1:3:xtic(2) notitle lc rgb \"#007000\" with boxes");
2024-08-06 14:49:57 +02:00
gnuplotObject.end(dataToFeed.join("\n"));
return gnuplotObject;
}
function antiXSS(string) {
return string.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
function getCount(period, callback) {
var query = "";
if (period == "daily") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);";
} else if (period == "weekly") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 WEEK);";
} else if (period == "monthly") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH);";
} else if (period == "yearly" || period == "annual") {
query = "SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 YEAR);";
} else if (period == "total" || period == "all") {
query = "SELECT COUNT(*) AS 'count' FROM entries;";
} else {
callback(true, -1);
return;
}
connection.query(query, function (error, results, fields) {
if (error) {
callServerError(500, error);
if (connection.end) connection.end();
return;
}
callback(false, parseInt(results[0].count));
});
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function formatTemplate(templateName, templateData, callback) {
readTemplate(templateName, function (data) {
callback(formatTemplateFromReadData(data, templateData));
});
}
function readTemplate(templateName, callback) {
fs.readFile(__dirname + "/../templates/" + templateName + ".template", function (err, data) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
callback(data);
});
}
function formatTemplateFromReadData(templateFileData, templateData) {
var tD = templateFileData.toString();
Object.keys(templateData).forEach(function (key) {
tD = tD.replace(new RegExp("\\{\\{" + escapeRegExp(key) + "\\}\\}", "g"), templateData[key]);
});
return tD;
}
if (href == "/") {
connection.connect(function (err) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
connection.query("SELECT id, ip, time, version, runtime, runtime_version FROM entries ORDER BY id DESC LIMIT 10;", function (error, results, fields) {
if (error) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
function getMods(callback, _id) {
if (!_id) _id = 0;
if (_id == results.length) {
callback();
return;
}
connection.query("SELECT name, version FROM entries_mods WHERE entry_id = " + mysql.escape(results[_id].id) + ";", function (error, mResults, mFields) {
if (error) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
results[_id].mods = mResults;
getMods(callback, _id + 1);
});
}
getMods(function () {
readTemplate("mods", function (modsData) {
readTemplate("mod", function (modData) {
readTemplate("lateststart", function (latestStartData) {
var latestStarts = "";
results.forEach(function (row) {
mods = "";
if (row.mods.length > 0) {
row.mods.forEach(function (mod) {
mods += formatTemplateFromReadData(modData, {
name: mod.name,
version: mod.version
});
});
mods = formatTemplateFromReadData(modsData, {
mods: mods
});
}
latestStarts += formatTemplateFromReadData(latestStartData, {
ipAddress: antiXSS(row.ip),
version: antiXSS(row.version),
runtime: antiXSS(row.runtime),
runtimeVersion: antiXSS(row.runtime_version),
mods: mods
});
});
getCount("daily", function (isBadRequest, dailyStarts) {
getCount("monthly", function (isBadRequest, monthlyStarts) {
getCount("yearly", function (isBadRequest, yearlyStarts) {
getCount("all", function (isBadRequest, totalStarts) {
formatTemplate("index", {
dailyStarts: dailyStarts,
monthlyStarts: monthlyStarts,
yearlyStarts: yearlyStarts,
totalStarts: totalStarts,
latestStarts: latestStarts
}, function (data) {
res.writeHead(200, {
"Content-Type": "text/html",
"Refresh": "60"
});
res.end(data);
if (connection.end) connection.end();
});
});
});
});
});
});
});
});
});
});
});
} else if (href == "/chart.svr") {
connection.connect(function (err) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
var query = "";
if (uobject.query.scope == "versiondistribution") {
query = "SELECT CONCAT('SVR.JS ', version) AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY version;";
} else if (uobject.query.scope == "modsdistribution") {
query = "SELECT entries_mods.name AS 'key', COUNT(*) AS 'value' FROM entries_mods INNER JOIN entries ON entries_mods.entry_id = entries.id WHERE entries.time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY entries_mods.name;";
} else if (uobject.query.scope == "jsruntimedistribution") {
query = "SELECT runtime AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY runtime;";
} else if (uobject.query.scope == "jsruntimeversiondistribution") {
query = "SELECT CONCAT(runtime, ' ', runtime_version) AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY runtime, runtime_version;";
} else {
callServerError(400);
if (connection.end) connection.end();
return;
}
connection.query(query, function (error, results, fields) {
if (error) {
callServerError(500, error);
if (connection.end) connection.end();
return;
}
var data = {};
results.forEach(function (keyValuePair) {
data[keyValuePair.key] = keyValuePair.value;
});
try {
var plotInstance = plot(data);
res.writeHead(200, {
"Content-Type": "image/png"
});
plotInstance.pipe(res);
if (connection.end) connection.end();
} catch (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
});
});
} else if (href == "/count.svr") {
connection.connect(function (err) {
if (err) {
callServerError(500, err);
if (connection.end) connection.end();
return;
}
getCount(uobject.query.period, function (isBadRequest, count) {
if (isBadRequest) {
callServerError(400);
if (connection.end) connection.end();
} else {
res.writeHead(200, {
"Content-Type": "text/plain"
});
res.end(count.toString());
if (connection.end) connection.end();
}
});
});
} else if (href == "/collect.svr") {
var headers = {
"Content-Type": "application/json"
};
if (req.method == "POST") {
if (req.headers["content-type"] && !req.headers["content-type"].match(/^application\/json(?:$|;)/)) {
headers["Accept"] = "application/json";
res.writeHead(415, headers);
res.end(JSON.stringify({
"status": 415,
"message": "Only JSON is supported."
}));
return;
}
var jsonData = "";
req.on("data", function (chunk) {
jsonData += chunk.toString();
});
req.on("end", function () {
try {
var parsedJsonData = {};
try {
parsedJsonData = JSON.parse(jsonData);
} catch (err) {
res.writeHead(400, headers);
res.end(JSON.stringify({
"status": 400,
"message": "JSON parse error."
}));
return;
}
var isValid = true;
if (!parsedJsonData.version || !parsedJsonData.runtime || !parsedJsonData.runtimeVersion || !parsedJsonData.mods || typeof parsedJsonData.version != "string" || typeof parsedJsonData.runtime != "string" || typeof parsedJsonData.runtimeVersion != "string" || !Array.isArray(parsedJsonData.mods)) {
isValid = false;
} else if (["Node.js", "Bun"].indexOf(parsedJsonData.runtime) == -1) {
isValid = false;
} else {
parsedJsonData.mods.every(function (element) {
if (!element.version || typeof element.name != "string" || typeof element.version != "string") {
isValid = false;
return false;
} else {
return true;
}
});
}
if (!isValid) {
res.writeHead(400, headers);
res.end(JSON.stringify({
"status": 400,
"message": "Invalid data."
}));
return;
}
connection.connect(function (err) {
if (err) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(err.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
if (connection.end) connection.end();
return;
}
var requestIP = (req.socket.realRemoteAddress ? req.socket.realRemoteAddress : req.socket.remoteAddress).replace(/^::ffff:/i, "");
connection.query("INSERT INTO entries (ip, time, version, runtime, runtime_version) VALUES (" + mysql.escape(requestIP) + ", NOW(), " + mysql.escape(parsedJsonData.version) + ", " + mysql.escape(parsedJsonData.runtime) + ", " + mysql.escape(parsedJsonData.runtimeVersion) + ");", function (error, results, fields) {
if (error) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(error.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
if (connection.end) connection.end();
return;
}
var entriesToInsert = [];
parsedJsonData.mods.forEach(function (mod) {
entriesToInsert.push("(" + mysql.escape(results.insertId) + ", " + mysql.escape(mod.name) + ", " + mysql.escape(mod.version) + ")");
});
connection.query(entriesToInsert.length > 0 ? ("INSERT INTO entries_mods (entry_id, name, version) VALUES " + entriesToInsert.join(", ") + ";") : "SELECT 1;", function (error, results, fields) {
2024-08-06 14:49:57 +02:00
if (error) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(error.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
if (connection.end) connection.end();
return;
}
res.writeHead(200, headers);
res.end(JSON.stringify({
"status": 200,
"message": "The statistics are added successfully."
}));
if (connection.end) connection.end();
});
});
});
} catch (err) {
serverconsole.errmessage("There was an error while processing the request!");
serverconsole.errmessage("Stack:");
serverconsole.errmessage(err.stack);
res.writeHead(500, headers);
res.end(JSON.stringify({
"status": 500,
"message": "An unexpected error occurred."
}));
}
});
} else {
headers["Allow"] = "POST";
res.writeHead(405, headers);
res.end(JSON.stringify({
"status": 405,
"message": "Invalid HTTP method."
}));
}
} else {
elseCallback();
}