From 209fa83cff4b35a955ba202c5079fdb9bad1cf57 Mon Sep 17 00:00:00 2001 From: LouisLam Date: Wed, 28 Jul 2021 00:52:31 +0800 Subject: [PATCH] Add Basic Auth for /metrics --- package-lock.json | 16 +++++++++++ package.json | 1 + server/auth.js | 40 ++++++++++++++++++++++++++++ server/model/monitor.js | 33 +++-------------------- server/prometheus.js | 59 +++++++++++++++++++++++++++++++++++++++++ server/server.js | 39 +++++++++++++++------------ 6 files changed, 141 insertions(+), 47 deletions(-) create mode 100644 server/auth.js create mode 100644 server/prometheus.js diff --git a/package-lock.json b/package-lock.json index 0a2a3a469..2fcc0feec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -625,6 +625,14 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, "bcrypt": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", @@ -1294,6 +1302,14 @@ } } }, + "express-basic-auth": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.0.tgz", + "integrity": "sha512-iJ0h1Gk6fZRrFmO7tP9nIbxwNgCUJASfNj5fb0Hy15lGtbqqsxpt7609+wq+0XlByZjXmC/rslWQtnuSTVRIcg==", + "requires": { + "basic-auth": "^2.0.1" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", diff --git a/package.json b/package.json index f0af413e0..b54c73ff9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "command-exists": "^1.2.9", "dayjs": "^1.10.6", "express": "^4.17.1", + "express-basic-auth": "^1.2.0", "form-data": "^4.0.0", "http-graceful-shutdown": "^3.1.2", "jsonwebtoken": "^8.5.1", diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 000000000..b4d13d68a --- /dev/null +++ b/server/auth.js @@ -0,0 +1,40 @@ +const basicAuth = require('express-basic-auth') +const passwordHash = require('./password-hash'); +const {R} = require("redbean-node"); + +/** + * + * @param username : string + * @param password : string + * @returns {Promise} + */ +exports.login = async function (username, password) { + let user = await R.findOne("user", " username = ? AND active = 1 ", [ + username + ]) + + if (user && passwordHash.verify(password, user.password)) { + // Upgrade the hash to bcrypt + if (passwordHash.needRehash(user.password)) { + await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ + passwordHash.generate(password), + user.id + ]); + } + return user; + } else { + return null; + } +} + +function myAuthorizer(username, password, callback) { + exports.login(username, password).then((user) => { + callback(null, user != null) + }) +} + +exports.basicAuth = basicAuth({ + authorizer: myAuthorizer, + authorizeAsync: true, + challenge: true +}); diff --git a/server/model/monitor.js b/server/model/monitor.js index 552149e57..27f3f9bab 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1,4 +1,3 @@ -const Prometheus = require('prom-client'); const https = require('https'); const dayjs = require("dayjs"); const utc = require('dayjs/plugin/utc') @@ -6,6 +5,7 @@ var timezone = require('dayjs/plugin/timezone') dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); +const {Prometheus} = require("../prometheus"); const {debug, UP, DOWN, PENDING} = require("../util"); const {tcping, ping, checkCertificate} = require("../util-server"); const {R} = require("redbean-node"); @@ -18,26 +18,6 @@ const customAgent = new https.Agent({ maxCachedSessions: 0 }); -const commonLabels = [ - 'monitor_name', - 'monitor_type', - 'monitor_url', - 'monitor_hostname', - 'monitor_port', -] - -const monitor_response_time = new Prometheus.Gauge({ - name: 'monitor_response_time', - help: 'Monitor Response Time (ms)', - labelNames: commonLabels -}); - -const monitor_status = new Prometheus.Gauge({ - name: 'monitor_status', - help: 'Monitor Status (1 = UP, 0= DOWN)', - labelNames: commonLabels -}); - /** * status: * 0 = DOWN @@ -76,13 +56,7 @@ class Monitor extends BeanModel { let previousBeat = null; let retries = 0; - const monitorLabelValues = { - monitor_name: this.name, - monitor_type: this.type, - monitor_url: this.url, - monitor_hostname: this.hostname, - monitor_port: this.port - } + let prometheus = new Prometheus(this); const beat = async () => { @@ -219,7 +193,6 @@ class Monitor extends BeanModel { bean.important = false; } - monitor_status.set(monitorLabelValues, bean.status) if (bean.status === UP) { console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) @@ -229,7 +202,7 @@ class Monitor extends BeanModel { console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) } - monitor_response_time.set(monitorLabelValues, bean.ping) + prometheus.update(bean) io.to(this.user_id).emit("heartbeat", bean.toJSON()); diff --git a/server/prometheus.js b/server/prometheus.js new file mode 100644 index 000000000..f60ec45a6 --- /dev/null +++ b/server/prometheus.js @@ -0,0 +1,59 @@ +const PrometheusClient = require('prom-client'); + +const commonLabels = [ + 'monitor_name', + 'monitor_type', + 'monitor_url', + 'monitor_hostname', + 'monitor_port', +] + +const monitor_response_time = new PrometheusClient.Gauge({ + name: 'monitor_response_time', + help: 'Monitor Response Time (ms)', + labelNames: commonLabels +}); + +const monitor_status = new PrometheusClient.Gauge({ + name: 'monitor_status', + help: 'Monitor Status (1 = UP, 0= DOWN)', + labelNames: commonLabels +}); + +class Prometheus { + monitorLabelValues = {} + + constructor(monitor) { + this.monitorLabelValues = { + monitor_name: monitor.name, + monitor_type: monitor.type, + monitor_url: monitor.url, + monitor_hostname: monitor.hostname, + monitor_port: monitor.port + } + } + + update(heartbeat) { + try { + monitor_status.set(this.monitorLabelValues, heartbeat.status) + } catch (e) { + console.error(e) + } + + try { + if (typeof heartbeat.ping === 'number') { + monitor_response_time.set(this.monitorLabelValues, heartbeat.ping) + } else { + // Is it good? + monitor_response_time.set(this.monitorLabelValues, -1) + } + } catch (e) { + console.error(e) + } + } + +} + +module.exports = { + Prometheus +} diff --git a/server/server.js b/server/server.js index 624e69be2..2014d927f 100644 --- a/server/server.js +++ b/server/server.js @@ -5,7 +5,6 @@ const http = require('http'); const { Server } = require("socket.io"); const dayjs = require("dayjs"); const {R} = require("redbean-node"); -const passwordHash = require('./password-hash'); const jwt = require('jsonwebtoken'); const Monitor = require("./model/monitor"); const fs = require("fs"); @@ -15,7 +14,9 @@ const gracefulShutdown = require('http-graceful-shutdown'); const Database = require("./database"); const {sleep} = require("./util"); const args = require('args-parser')(process.argv); -const apiMetrics = require('prometheus-api-metrics'); +const prometheusAPIMetrics = require('prometheus-api-metrics'); +const { basicAuth } = require("./auth"); +const {login} = require("./auth"); const version = require('../package.json').version; const hostname = args.host || "0.0.0.0" const port = args.port || 3001 @@ -27,6 +28,9 @@ const app = express(); const server = http.createServer(app); const io = new Server(server); app.use(express.json()) +const basicAuthRouter = express.Router(); +basicAuthRouter.use(basicAuth) +app.use(basicAuthRouter) /** * Total WebSocket client connected to server currently, no actual use @@ -56,15 +60,27 @@ let needSetup = false; await initDatabase(); console.log("Adding route") + + // Normal Router here + app.use('/', express.static("dist")); - app.use(apiMetrics()) + // Basic Auth Router here + // For testing + basicAuthRouter.get('/test-auth', (req, res) => { + res.end("OK") + }); + + // Prometheus API metrics /metrics + // With Basic Auth using the first user's username/password + basicAuthRouter.use(prometheusAPIMetrics()) + + // Universal Route Handler, must be at the end app.get('*', function(request, response, next) { response.sendFile(process.cwd() + '/dist/index.html'); }); - console.log("Adding socket handler") io.on('connection', async (socket) => { @@ -120,20 +136,9 @@ let needSetup = false; socket.on("login", async (data, callback) => { console.log("Login") - let user = await R.findOne("user", " username = ? AND active = 1 ", [ - data.username - ]) - - if (user && passwordHash.verify(data.password, user.password)) { - - // Upgrade the hash to bcrypt - if (passwordHash.needRehash(user.password)) { - await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ - passwordHash.generate(data.password), - user.id - ]); - } + let user = await login(data.username, data.password) + if (user) { await afterLogin(socket, user) callback({