Merge branch 'master' into master

This commit is contained in:
Moritz R
2022-06-15 11:33:00 +02:00
committed by GitHub
66 changed files with 4998 additions and 842 deletions

View File

@@ -59,6 +59,8 @@ class Database {
"patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
};
/**

View File

@@ -7,7 +7,7 @@ dayjs.extend(timezone);
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@@ -17,6 +17,12 @@ const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const axiosCachedDnsResolve = require("esm-wallaby")(module)("axios-cached-dns-resolve");
// create an axios client instance with the cached DNS resolve interceptor
const axiosClient = axios.create();
axiosCachedDnsResolve.registerInterceptor(axiosClient);
/**
* status:
* 0 = DOWN
@@ -91,7 +97,12 @@ class Monitor extends BeanModel {
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage
mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
};
if (includeSensitiveData) {
@@ -196,7 +207,7 @@ class Monitor extends BeanModel {
let bean = R.dispense("heartbeat");
bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc());
bean.time = R.isoDateTimeMillis(dayjs.utc());
bean.status = DOWN;
if (this.isUpsideDown()) {
@@ -217,7 +228,7 @@ class Monitor extends BeanModel {
// HTTP basic auth
let basicAuthHeader = {};
if (this.basic_auth_user) {
if (this.auth_method === "basic") {
basicAuthHeader = {
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
};
@@ -268,7 +279,21 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options);
let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axiosClient.request(options);
}
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;
@@ -316,7 +341,11 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but keyword is not found");
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
}
}
@@ -371,22 +400,33 @@ class Monitor extends BeanModel {
bean.msg = dnsMessage;
bean.status = UP;
} else if (this.type === "push") { // Type: Push
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
const bufferTime = 1000; // 1s buffer to accommodate clock differences
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
this.id,
time
]);
if (previousBeat) {
const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
if (heartbeatCount <= 0) {
throw new Error("No heartbeat in the time window");
// If the previous beat was down or pending we use the regular
// beatInterval/retryInterval in the setTimeout further below
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
throw new Error("No heartbeat in the time window");
} else {
let timeout = beatInterval * 1000 - msSinceLastBeat;
if (timeout < 0) {
timeout = bufferTime;
} else {
timeout += bufferTime;
}
// No need to insert successful heartbeat for push type, so end here
retries = 0;
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
this.heartbeatInterval = setTimeout(beat, timeout);
return;
}
} else {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return;
throw new Error("No heartbeat in the time window");
}
} else if (this.type === "steam") {
@@ -398,7 +438,7 @@ class Monitor extends BeanModel {
throw new Error("Steam API Key not found");
}
let res = await axios.get(steamApiUrl, {
let res = await axiosClient.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
@@ -463,6 +503,14 @@ class Monitor extends BeanModel {
interval: this.interval,
});
bean.status = UP;
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
@@ -513,7 +561,7 @@ class Monitor extends BeanModel {
}
if (bean.status === UP) {
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) {
if (this.retryInterval > 0) {
beatInterval = this.retryInterval;
@@ -857,10 +905,19 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);
log.debug("monitor", "call sendCertNotificationByTargetDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
}
if (notifyDays != null && Array.isArray(notifyDays)) {
for (const day of notifyDays) {
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
}
}
}
}

View File

@@ -1,10 +1,104 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class StatusPage extends BeanModel {
/**
* Like this: { "test-uptime.kuma.pet": "default" }
* @type {{}}
*/
static domainMappingList = { };
/**
*
* @param {Response} response
* @param {string} indexHTML
* @param {string} slug
*/
static async handleStatusPageResponse(response, indexHTML, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (statusPage) {
response.send(await StatusPage.renderHTML(indexHTML, statusPage));
} else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
}
}
/**
* SSR for status pages
* @param {string} indexHTML
* @param {StatusPage} statusPage
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = statusPage.description?.substring(0, 155);
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
if (statusPage.icon) {
$("link[rel=icon]")
.attr("href", statusPage.icon)
.removeAttr("type");
}
const head = $("head");
// OG Meta Tags
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
head.append(`<meta property="og:description" content="${description155}" />`);
// Preload data
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
head.append(`
<script>
window.preloadData = ${json}
</script>
`);
return $.root().html();
}
/**
* Get all status page data in one call
* @param {StatusPage} statusPage
*/
static async getStatusPageData(statusPage) {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
return {
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
};
}
/**
* Loads domain mapping from DB
* Return object like this: { "test-uptime.kuma.pet": "default" }

View File

@@ -0,0 +1,26 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Ntfy extends NotificationProvider {
name = "ntfy";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`${notification.ntfyserverurl}`, {
"topic": notification.ntfytopic,
"message": msg,
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Ntfy;

View File

@@ -2,6 +2,7 @@ const { R } = require("redbean-node");
const Apprise = require("./notification-providers/apprise");
const Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify");
const Ntfy = require("./notification-providers/ntfy");
const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost");
@@ -52,6 +53,7 @@ class Notification {
new Discord(),
new Teams(),
new Gotify(),
new Ntfy(),
new Line(),
new LunaSea(),
new Feishu(),

View File

@@ -1,5 +1,5 @@
let express = require("express");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = require("../util-server");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
@@ -59,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let duration = 0;
let bean = R.dispense("heartbeat");
bean.time = R.isoDateTime(dayjs.utc());
bean.time = R.isoDateTimeMillis(dayjs.utc());
if (previousHeartbeat) {
isFirstBeat = false;
@@ -67,6 +67,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
log.debug("router", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status);
@@ -91,115 +92,13 @@ router.get("/api/push/:pushToken", async (request, response) => {
}
} catch (e) {
response.json({
response.status(404).json({
ok: false,
msg: e.message
});
}
});
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
try {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
response.json({
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
allowAllOrigin(response);
@@ -376,16 +275,4 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
}
});
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
function send403(res, msg = "") {
res.status(403).json({
"status": "fail",
"msg": msg,
});
}
module.exports = router;

View File

@@ -0,0 +1,110 @@
let express = require("express");
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, send403 } = require("../util-server");
const { R } = require("redbean-node");
const Monitor = require("../model/monitor");
let router = express.Router();
let cache = apicache.middleware;
const server = UptimeKumaServer.getInstance();
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status-page", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
try {
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
return null;
}
let statusPageData = await StatusPage.getStatusPageData(statusPage);
if (!statusPageData) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
// Response
response.json(statusPageData);
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
module.exports = router;

View File

@@ -16,7 +16,7 @@ if (nodeVersion < requiredVersion) {
}
const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config");
log.info("server", "Welcome to Uptime Kuma");
@@ -35,6 +35,7 @@ const fs = require("fs");
log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express");
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken");
@@ -148,22 +149,6 @@ let jwtSecret = null;
*/
let needSetup = false;
/**
* Cache Index HTML
* @type {string}
*/
let indexHTML = "";
try {
indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
(async () => {
Database.init(args);
await initDatabase(testMode);
@@ -179,13 +164,17 @@ try {
// Entry Page
app.get("/", async (request, response) => {
debug(`Request Domain: ${request.hostname}`);
log.debug("entry", `Request Domain: ${request.hostname}`);
if (request.hostname in StatusPage.domainMappingList) {
debug("This is a status page domain");
response.send(indexHTML);
log.debug("entry", "This is a status page domain");
let slug = StatusPage.domainMappingList[request.hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else {
response.redirect("/dashboard");
}
@@ -214,7 +203,9 @@ try {
// With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist"));
app.use("/", expressStaticGzip("dist", {
enableBrotli: true,
}));
// ./data/upload
app.use("/upload", express.static(Database.uploadDir));
@@ -227,12 +218,16 @@ try {
const apiRouter = require("./routers/api-router");
app.use(apiRouter);
// Status Page Router
const statusPageRouter = require("./routers/status-page-router");
app.use(statusPageRouter);
// Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found.");
} else {
response.send(indexHTML);
response.send(server.indexHTML);
}
});
@@ -677,6 +672,11 @@ try {
bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.databaseConnectionString = monitor.databaseConnectionString;
bean.databaseQuery = monitor.databaseQuery;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
await R.store(bean);
@@ -1250,8 +1250,11 @@ try {
method: monitorListData[i].method || "GET",
body: monitorListData[i].body,
headers: monitorListData[i].headers,
authMethod: monitorListData[i].authMethod,
basic_auth_user: monitorListData[i].basic_auth_user,
basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
interval: monitorListData[i].interval,
retryInterval: retryInterval,
hostname: monitorListData[i].hostname,

View File

@@ -29,6 +29,12 @@ class UptimeKumaServer {
httpServer = undefined;
io = undefined;
/**
* Cache Index HTML
* @type {string}
*/
indexHTML = "";
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@@ -55,6 +61,16 @@ class UptimeKumaServer {
this.httpServer = http.createServer(this.app);
}
try {
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
this.io = new Server(this.httpServer);
}

View File

@@ -10,6 +10,8 @@ const chardet = require("chardet");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { NtlmClient } = require("axios-ntlm");
// From ping-lite
exports.WIN = /^win/.test(process.platform);
@@ -172,6 +174,26 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
});
};
/**
* Use NTLM Auth for a http request.
* @param {Object} options The http request options
* @param {Object} ntlmOptions The auth options
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.httpNtlm = function (options, ntlmOptions) {
return new Promise((resolve, reject) => {
let client = NtlmClient(ntlmOptions);
client(options)
.then((resp) => {
resolve(resp);
})
.catch((err) => {
reject(err);
});
});
};
/**
* Resolves a given record using the specified DNS server
* @param {string} hostname The hostname of the record to lookup
@@ -185,7 +207,7 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
// Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300)
resolverServer = resolverServer.replace("[", "").replace("]", "");
resolver.setServers([`[${resolverServer}]:${resolverPort}`]);
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
return new Promise((resolve, reject) => {
if (rrtype === "PTR") {
resolver.reverse(hostname, (err, records) => {
@@ -207,6 +229,31 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
});
};
/**
* Run a query on SQL Server
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
mssql.on("error", err => {
reject(err);
});
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
};
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
@@ -558,3 +605,15 @@ exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
exports.filterAndJoin = (parts, connector = "") => {
return parts.filter((part) => !!part && part !== "").join(connector);
};
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
module.exports.send403 = (res, msg = "") => {
res.status(403).json({
"status": "fail",
"msg": msg,
});
};