Add Basic Auth for /metrics

This commit is contained in:
LouisLam 2021-07-28 00:52:31 +08:00
parent cc6f1d7487
commit 209fa83cff
6 changed files with 141 additions and 47 deletions

16
package-lock.json generated
View file

@ -625,6 +625,14 @@
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" "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": { "bcrypt": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", "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": { "extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",

View file

@ -32,6 +32,7 @@
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"express": "^4.17.1", "express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.2", "http-graceful-shutdown": "^3.1.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",

40
server/auth.js Normal file
View file

@ -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<Bean|null>}
*/
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
});

View file

@ -1,4 +1,3 @@
const Prometheus = require('prom-client');
const https = require('https'); const https = require('https');
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
@ -6,6 +5,7 @@ var timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const axios = require("axios"); const axios = require("axios");
const {Prometheus} = require("../prometheus");
const {debug, UP, DOWN, PENDING} = require("../util"); const {debug, UP, DOWN, PENDING} = require("../util");
const {tcping, ping, checkCertificate} = require("../util-server"); const {tcping, ping, checkCertificate} = require("../util-server");
const {R} = require("redbean-node"); const {R} = require("redbean-node");
@ -18,26 +18,6 @@ const customAgent = new https.Agent({
maxCachedSessions: 0 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: * status:
* 0 = DOWN * 0 = DOWN
@ -76,13 +56,7 @@ class Monitor extends BeanModel {
let previousBeat = null; let previousBeat = null;
let retries = 0; let retries = 0;
const monitorLabelValues = { let prometheus = new Prometheus(this);
monitor_name: this.name,
monitor_type: this.type,
monitor_url: this.url,
monitor_hostname: this.hostname,
monitor_port: this.port
}
const beat = async () => { const beat = async () => {
@ -219,7 +193,6 @@ class Monitor extends BeanModel {
bean.important = false; bean.important = false;
} }
monitor_status.set(monitorLabelValues, bean.status)
if (bean.status === UP) { if (bean.status === UP) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) 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}`) 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()); io.to(this.user_id).emit("heartbeat", bean.toJSON());

59
server/prometheus.js Normal file
View file

@ -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
}

View file

@ -5,7 +5,6 @@ const http = require('http');
const { Server } = require("socket.io"); const { Server } = require("socket.io");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const {R} = require("redbean-node"); const {R} = require("redbean-node");
const passwordHash = require('./password-hash');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
const fs = require("fs"); const fs = require("fs");
@ -15,7 +14,9 @@ const gracefulShutdown = require('http-graceful-shutdown');
const Database = require("./database"); const Database = require("./database");
const {sleep} = require("./util"); const {sleep} = require("./util");
const args = require('args-parser')(process.argv); 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 version = require('../package.json').version;
const hostname = args.host || "0.0.0.0" const hostname = args.host || "0.0.0.0"
const port = args.port || 3001 const port = args.port || 3001
@ -27,6 +28,9 @@ const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const io = new Server(server); const io = new Server(server);
app.use(express.json()) app.use(express.json())
const basicAuthRouter = express.Router();
basicAuthRouter.use(basicAuth)
app.use(basicAuthRouter)
/** /**
* Total WebSocket client connected to server currently, no actual use * Total WebSocket client connected to server currently, no actual use
@ -56,15 +60,27 @@ let needSetup = false;
await initDatabase(); await initDatabase();
console.log("Adding route") console.log("Adding route")
// Normal Router here
app.use('/', express.static("dist")); 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) { app.get('*', function(request, response, next) {
response.sendFile(process.cwd() + '/dist/index.html'); response.sendFile(process.cwd() + '/dist/index.html');
}); });
console.log("Adding socket handler") console.log("Adding socket handler")
io.on('connection', async (socket) => { io.on('connection', async (socket) => {
@ -120,20 +136,9 @@ let needSetup = false;
socket.on("login", async (data, callback) => { socket.on("login", async (data, callback) => {
console.log("Login") console.log("Login")
let user = await R.findOne("user", " username = ? AND active = 1 ", [ let user = await login(data.username, data.password)
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
]);
}
if (user) {
await afterLogin(socket, user) await afterLogin(socket, user)
callback({ callback({