uptime-kuma/server/util-server.js

412 lines
11 KiB
JavaScript
Raw Normal View History

2021-07-27 18:47:13 +01:00
const tcpp = require("tcp-ping");
2021-07-01 10:00:23 +01:00
const Ping = require("./ping-lite");
2021-07-27 18:47:13 +01:00
const { R } = require("redbean-node");
const { debug } = require("../src/util");
2021-08-09 06:34:44 +01:00
const passwordHash = require("./password-hash");
const dayjs = require("dayjs");
2021-08-23 15:30:11 +01:00
const { Resolver } = require("dns");
const child_process = require("child_process");
const iconv = require("iconv-lite");
const chardet = require("chardet");
const fs = require("fs");
const nodeJsUtil = require("util");
2021-11-04 01:46:43 +00:00
const mqtt = require("mqtt");
2021-08-09 06:34:44 +01:00
// From ping-lite
exports.WIN = /^win/.test(process.platform);
exports.LIN = /^linux/.test(process.platform);
exports.MAC = /^darwin/.test(process.platform);
2022-01-11 17:44:01 +00:00
exports.FBSD = /^freebsd/.test(process.platform);
2022-01-09 16:27:24 +00:00
exports.BSD = /bsd$/.test(process.platform);
2021-08-09 06:34:44 +01:00
/**
* Init or reset JWT secret
* @returns {Promise<Bean>}
*/
exports.initJWTSecret = async () => {
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
]);
2021-11-04 01:46:43 +00:00
if (!jwtSecretBean) {
2021-08-09 06:34:44 +01:00
jwtSecretBean = R.dispense("setting");
jwtSecretBean.key = "jwtSecret";
}
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
await R.store(jwtSecretBean);
return jwtSecretBean;
2021-09-20 09:22:18 +01:00
};
2021-07-01 07:03:06 +01:00
exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => {
tcpp.ping({
address: hostname,
port: port,
attempts: 1,
}, function (err, data) {
2021-07-01 07:03:06 +01:00
if (err) {
reject(err);
}
if (data.results.length >= 1 && data.results[0].err) {
reject(data.results[0].err);
}
resolve(Math.round(data.max));
});
});
2021-09-20 09:22:18 +01:00
};
2021-07-01 10:00:23 +01:00
exports.ping = async (hostname) => {
try {
2021-08-10 15:00:29 +01:00
return await exports.pingAsync(hostname);
} catch (e) {
// If the host cannot be resolved, try again with ipv6
if (e.message.includes("service not known")) {
2021-08-10 15:00:29 +01:00
return await exports.pingAsync(hostname, true);
} else {
throw e;
}
}
2021-09-20 09:22:18 +01:00
};
exports.pingAsync = function (hostname, ipv6 = false) {
2021-07-01 10:00:23 +01:00
return new Promise((resolve, reject) => {
const ping = new Ping(hostname, {
ipv6
});
2021-07-01 10:00:23 +01:00
ping.send(function (err, ms, stdout) {
2021-07-01 10:00:23 +01:00
if (err) {
reject(err);
2021-07-01 10:00:23 +01:00
} else if (ms === null) {
2021-09-20 09:22:18 +01:00
reject(new Error(stdout));
2021-07-01 10:00:23 +01:00
} else {
2021-09-20 09:22:18 +01:00
resolve(Math.round(ms));
2021-07-01 10:00:23 +01:00
}
});
});
2021-09-20 09:22:18 +01:00
};
2021-07-09 07:14:03 +01:00
exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
2021-11-04 01:46:43 +00:00
return new Promise((resolve, reject) => {
const { port, username, password, interval = 20 } = options;
2021-11-04 01:46:43 +00:00
try {
// Adds MQTT protocol to the hostname if not already present
2021-12-24 00:39:47 +00:00
if (!/^(?:http|mqtt)s?:\/\//.test(hostname)) {
hostname = "mqtt://" + hostname;
}
2021-11-17 01:44:10 +00:00
let client = mqtt.connect(hostname, {
port,
username,
password
});
2021-11-04 01:46:43 +00:00
client.on("connect", () => {
client.subscribe(topic);
});
2021-11-04 01:46:43 +00:00
client.on("message", (messageTopic, message) => {
2021-11-17 01:44:10 +00:00
if (messageTopic == topic) {
if (message.toString() === okMessage) {
client.end();
resolve(`Topic: ${messageTopic}; Message: ${message.toString()}`);
} else {
client.end();
reject(new Error(`Error; Topic: ${messageTopic}; Message: ${message.toString()}`));
}
2021-11-04 01:46:43 +00:00
}
});
setTimeout(() => {
client.end();
}, interval * 1000);
2021-11-04 01:46:43 +00:00
} catch (error) {
reject(new Error(error));
}
});
};
2021-08-22 23:05:48 +01:00
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
const resolver = new Resolver();
resolver.setServers([resolver_server]);
return new Promise((resolve, reject) => {
2021-08-23 15:30:11 +01:00
if (rrtype == "PTR") {
2021-08-22 23:05:48 +01:00
resolver.reverse(hostname, (err, records) => {
if (err) {
reject(err);
} else {
resolve(records);
}
});
} else {
resolver.resolve(hostname, rrtype, (err, records) => {
if (err) {
reject(err);
} else {
resolve(records);
}
});
}
2021-09-20 09:22:18 +01:00
});
};
2021-08-22 23:05:48 +01:00
2021-07-09 07:14:03 +01:00
exports.setting = async function (key) {
2021-07-31 15:02:30 +01:00
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
2021-07-27 18:47:13 +01:00
key,
2021-07-31 15:02:30 +01:00
]);
try {
const v = JSON.parse(value);
2021-09-20 09:22:18 +01:00
debug(`Get Setting: ${key}: ${v}`);
return v;
2021-07-31 15:02:30 +01:00
} catch (e) {
return value;
}
2021-09-20 09:22:18 +01:00
};
2021-07-09 07:14:03 +01:00
2021-10-09 17:16:13 +01:00
exports.setSetting = async function (key, value, type = null) {
2021-07-21 19:02:35 +01:00
let bean = await R.findOne("setting", " `key` = ? ", [
2021-07-27 18:47:13 +01:00
key,
2021-09-20 09:22:18 +01:00
]);
if (!bean) {
2021-09-20 09:22:18 +01:00
bean = R.dispense("setting");
2021-07-21 19:02:35 +01:00
bean.key = key;
}
2021-10-09 17:16:13 +01:00
bean.type = type;
2021-07-31 15:02:30 +01:00
bean.value = JSON.stringify(value);
2021-09-20 09:22:18 +01:00
await R.store(bean);
};
2021-07-21 19:02:35 +01:00
2021-07-09 07:14:03 +01:00
exports.getSettings = async function (type) {
2021-07-31 14:57:58 +01:00
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
2021-07-27 18:47:13 +01:00
type,
2021-09-20 09:22:18 +01:00
]);
2021-07-09 07:14:03 +01:00
let result = {};
for (let row of list) {
2021-07-31 15:02:30 +01:00
try {
result[row.key] = JSON.parse(row.value);
} catch (e) {
result[row.key] = row.value;
}
2021-07-09 07:14:03 +01:00
}
return result;
2021-09-20 09:22:18 +01:00
};
2021-07-31 14:57:58 +01:00
exports.setSettings = async function (type, data) {
let keyList = Object.keys(data);
let promiseList = [];
for (let key of keyList) {
let bean = await R.findOne("setting", " `key` = ? ", [
key
]);
if (bean == null) {
bean = R.dispense("setting");
bean.type = type;
bean.key = key;
}
if (bean.type === type) {
bean.value = JSON.stringify(data[key]);
2021-09-20 09:22:18 +01:00
promiseList.push(R.store(bean));
2021-07-31 14:57:58 +01:00
}
}
await Promise.all(promiseList);
2021-09-20 09:22:18 +01:00
};
2021-07-31 14:57:58 +01:00
// ssl-checker by @dyaa
// param: res - response object from axios
// return an object containing the certificate information
const getDaysBetween = (validFrom, validTo) =>
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
const getDaysRemaining = (validFrom, validTo) => {
const daysRemaining = getDaysBetween(validFrom, validTo);
if (new Date(validTo).getTime() < new Date().getTime()) {
return -daysRemaining;
}
return daysRemaining;
};
2021-10-01 11:44:32 +01:00
// Fix certificate Info for display
// param: info - the chain obtained from getPeerCertificate()
const parseCertificateInfo = function (info) {
let link = info;
let i = 0;
const existingList = {};
2021-10-01 11:44:32 +01:00
while (link) {
debug(`[${i}] ${link.fingerprint}`);
2021-10-01 11:44:32 +01:00
if (!link.valid_from || !link.valid_to) {
break;
}
link.validTo = new Date(link.valid_to);
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
existingList[link.fingerprint] = true;
2021-10-01 11:44:32 +01:00
// Move up the chain until loop is encountered
if (link.issuerCertificate == null) {
break;
} else if (link.issuerCertificate.fingerprint in existingList) {
debug(`[Last] ${link.issuerCertificate.fingerprint}`);
2021-10-01 11:44:32 +01:00
link.issuerCertificate = null;
break;
} else {
link = link.issuerCertificate;
}
// Should be no use, but just in case.
if (i > 500) {
throw new Error("Dead loop occurred in parseCertificateInfo");
}
i++;
}
2021-10-01 11:44:32 +01:00
return info;
};
2021-10-01 11:44:32 +01:00
exports.checkCertificate = function (res) {
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
2021-11-08 07:51:32 +00:00
debug("Parsing Certificate Info");
2021-10-01 11:44:32 +01:00
const parsedInfo = parseCertificateInfo(info);
return {
2021-10-01 11:44:32 +01:00
valid: valid,
certInfo: parsedInfo
};
2021-09-20 09:22:18 +01:00
};
// Check if the provided status code is within the accepted ranges
// Param: status - the status code to check
// Param: accepted_codes - an array of accepted status codes
// Return: true if the status code is within the accepted ranges, false otherwise
// Will throw an error if the provided status code is not a valid range string or code string
exports.checkStatusCode = function (status, accepted_codes) {
if (accepted_codes == null || accepted_codes.length === 0) {
return false;
}
for (const code_range of accepted_codes) {
const code_range_split = code_range.split("-").map(string => parseInt(string));
if (code_range_split.length === 1) {
if (status === code_range_split[0]) {
return true;
}
} else if (code_range_split.length === 2) {
if (status >= code_range_split[0] && status <= code_range_split[1]) {
return true;
}
} else {
throw new Error("Invalid status code range");
}
}
return false;
2021-09-20 09:22:18 +01:00
};
exports.getTotalClientInRoom = (io, roomName) => {
const sockets = io.sockets;
2021-11-04 01:46:43 +00:00
if (!sockets) {
return 0;
}
const adapter = sockets.adapter;
2021-11-04 01:46:43 +00:00
if (!adapter) {
return 0;
}
const room = adapter.rooms.get(roomName);
if (room) {
return room.size;
} else {
return 0;
}
2021-09-20 09:22:18 +01:00
};
2021-09-11 12:40:03 +01:00
exports.allowDevAllOrigin = (res) => {
if (process.env.NODE_ENV === "development") {
exports.allowAllOrigin(res);
}
2021-09-20 09:22:18 +01:00
};
2021-09-11 12:40:03 +01:00
exports.allowAllOrigin = (res) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
2021-09-20 09:22:18 +01:00
};
2021-09-16 15:48:28 +01:00
exports.checkLogin = (socket) => {
2021-11-04 01:46:43 +00:00
if (!socket.userID) {
2021-09-16 15:48:28 +01:00
throw new Error("You are not logged in.");
}
2021-09-20 09:22:18 +01:00
};
exports.startUnitTest = async () => {
console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const child = child_process.spawn(npm, ["run", "jest"]);
child.stdout.on("data", (data) => {
console.log(data.toString());
});
child.stderr.on("data", (data) => {
console.log(data.toString());
});
child.on("close", function (code) {
console.log("Jest exit code: " + code);
2021-10-05 13:40:40 +01:00
process.exit(code);
});
};
/**
* @param body : Buffer
* @returns {string}
*/
exports.convertToUTF8 = (body) => {
const guessEncoding = chardet.detect(body);
2021-10-15 11:36:40 +01:00
//debug("Guess Encoding: " + guessEncoding);
const str = iconv.decode(body, guessEncoding);
return str.toString();
};
2021-10-29 11:28:31 +01:00
let logFile;
try {
logFile = fs.createWriteStream("./data/error.log", {
flags: "a"
});
} catch (_) { }
exports.errorLog = (error, outputToConsole = true) => {
try {
2021-10-29 11:28:31 +01:00
if (logFile) {
const dateTime = R.isoDateTime();
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
2021-10-29 11:28:31 +01:00
if (outputToConsole) {
console.error(error);
}
}
} catch (_) { }
};