mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-24 15:24:05 +00:00
Update auth and skeleton of wrapper
This commit is contained in:
parent
5773eeb6df
commit
6d2f624242
3 changed files with 208 additions and 22 deletions
100
server/auth.js
100
server/auth.js
|
@ -36,20 +36,32 @@ exports.login = async function (username, password) {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uk prefix + key ID is before _
|
||||||
|
* @param {string} key API Key
|
||||||
|
* @returns {{clear: string, index: string}} Parsed API key
|
||||||
|
*/
|
||||||
|
exports.parseAPIKey = function (key) {
|
||||||
|
let index = key.substring(2, key.indexOf("_"));
|
||||||
|
let clear = key.substring(key.indexOf("_") + 1, key.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
clear,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a provided API key
|
* Validate a provided API key
|
||||||
* @param {string} key API key to verify
|
* @param {string} key API key to verify
|
||||||
* @returns {boolean} API is ok?
|
* @returns {Promise<boolean>} API is ok?
|
||||||
*/
|
*/
|
||||||
async function verifyAPIKey(key) {
|
async function verifyAPIKey(key) {
|
||||||
if (typeof key !== "string") {
|
if (typeof key !== "string") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// uk prefix + key ID is before _
|
const { index, clear } = exports.parseAPIKey(key);
|
||||||
let index = key.substring(2, key.indexOf("_"));
|
|
||||||
let clear = key.substring(key.indexOf("_") + 1, key.length);
|
|
||||||
|
|
||||||
let hash = await R.findOne("api_key", " id=? ", [ index ]);
|
let hash = await R.findOne("api_key", " id=? ", [ index ]);
|
||||||
|
|
||||||
if (hash === null) {
|
if (hash === null) {
|
||||||
|
@ -65,6 +77,28 @@ async function verifyAPIKey(key) {
|
||||||
return hash && passwordHash.verify(clear, hash.key);
|
return hash && passwordHash.verify(clear, hash.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key API key to verify
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If API key is invalid or rate limit exceeded
|
||||||
|
*/
|
||||||
|
async function verifyAPIKeyWithRateLimit(key) {
|
||||||
|
const pass = await apiRateLimiter.pass(null, 0);
|
||||||
|
if (pass) {
|
||||||
|
await apiRateLimiter.removeTokens(1);
|
||||||
|
const valid = await verifyAPIKey(key);
|
||||||
|
if (!valid) {
|
||||||
|
const errMsg = "Failed API auth attempt: invalid API Key";
|
||||||
|
log.warn("api-auth", errMsg);
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errMsg = "Failed API auth attempt: rate limit exceeded";
|
||||||
|
log.warn("api-auth", errMsg);
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for basic auth authorizers
|
* Callback for basic auth authorizers
|
||||||
* @callback authCallback
|
* @callback authCallback
|
||||||
|
@ -80,22 +114,10 @@ async function verifyAPIKey(key) {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function apiAuthorizer(username, password, callback) {
|
function apiAuthorizer(username, password, callback) {
|
||||||
// API Rate Limit
|
verifyAPIKeyWithRateLimit(password).then(() => {
|
||||||
apiRateLimiter.pass(null, 0).then((pass) => {
|
callback(null, true);
|
||||||
if (pass) {
|
}).catch(() => {
|
||||||
verifyAPIKey(password).then((valid) => {
|
|
||||||
if (!valid) {
|
|
||||||
log.warn("api-auth", "Failed API auth attempt: invalid API Key");
|
|
||||||
}
|
|
||||||
callback(null, valid);
|
|
||||||
// Only allow a set number of api requests per minute
|
|
||||||
// (currently set to 60)
|
|
||||||
apiRateLimiter.removeTokens(1);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
|
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,3 +185,41 @@ exports.basicAuthMiddleware = async function (req, res, next) {
|
||||||
});
|
});
|
||||||
middleware(req, res, next);
|
middleware(req, res, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the API key from the header Authorization and verify it
|
||||||
|
exports.headerAuthMiddleware = async function (req, res, next) {
|
||||||
|
const authorizationHeader = req.header("Authorization");
|
||||||
|
|
||||||
|
let key = null;
|
||||||
|
|
||||||
|
if (authorizationHeader && typeof authorizationHeader === "string") {
|
||||||
|
const arr = authorizationHeader.split(" ");
|
||||||
|
if (arr.length === 2) {
|
||||||
|
const type = arr[0];
|
||||||
|
if (type === "Bearer") {
|
||||||
|
key = arr[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
try {
|
||||||
|
await verifyAPIKeyWithRateLimit(key);
|
||||||
|
res.locals.apiKeyID = exports.parseAPIKey(key).index;
|
||||||
|
next();
|
||||||
|
} catch (e) {
|
||||||
|
res.status(401);
|
||||||
|
res.json({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await apiRateLimiter.removeTokens(1);
|
||||||
|
res.status(401);
|
||||||
|
res.json({
|
||||||
|
ok: false,
|
||||||
|
msg: "No API Key provided, please provide an API Key in the \"Authorization\" header",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
let express = require("express");
|
const express = require("express");
|
||||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
|
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
@ -12,6 +12,10 @@ const { badgeConstants } = require("../config");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const { UptimeCalculator } = require("../uptime-calculator");
|
const { UptimeCalculator } = require("../uptime-calculator");
|
||||||
|
const ioClient = require("socket.io-client").io;
|
||||||
|
const Socket = require("socket.io-client").Socket;
|
||||||
|
const { headerAuthMiddleware } = require("../auth");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
|
@ -109,6 +113,117 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Map Socket.io API to REST API
|
||||||
|
*/
|
||||||
|
router.post("/api", headerAuthMiddleware, async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
// TODO: Allow whitelist of origins
|
||||||
|
|
||||||
|
// Generate a JWT for logging in to the socket.io server
|
||||||
|
const apiKeyID = response.locals.apiKeyID;
|
||||||
|
const userID = await R.getCell("SELECT user_id FROM api_key WHERE id = ?", [ apiKeyID ]);
|
||||||
|
const username = await R.getCell("SELECT username FROM user WHERE id = ?", [ userID ]);
|
||||||
|
const token = jwt.sign({
|
||||||
|
username,
|
||||||
|
}, server.jwtSecret);
|
||||||
|
|
||||||
|
const requestData = request.body;
|
||||||
|
|
||||||
|
console.log(requestData);
|
||||||
|
|
||||||
|
// TODO: should not hard coded
|
||||||
|
let wsURL = "ws://localhost:3001";
|
||||||
|
|
||||||
|
const socket = ioClient(wsURL, {
|
||||||
|
transports: [ "websocket" ],
|
||||||
|
reconnection: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result = await socketClientHandler(socket, token, requestData);
|
||||||
|
let status = 404;
|
||||||
|
if (result.status) {
|
||||||
|
status = result.status;
|
||||||
|
} else if (result.ok) {
|
||||||
|
status = 200;
|
||||||
|
}
|
||||||
|
response.status(status).json(result);
|
||||||
|
} catch (e) {
|
||||||
|
response.status(e.status).json(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Close socket");
|
||||||
|
socket.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Socket} socket
|
||||||
|
* @param {string} token JWT
|
||||||
|
* @param {object} requestData Request Data
|
||||||
|
*/
|
||||||
|
function socketClientHandler(socket, token, requestData) {
|
||||||
|
const action = requestData.action;
|
||||||
|
const params = requestData.params;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
socket.on("connect", () => {
|
||||||
|
socket.emit("loginByToken", token, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
|
||||||
|
if (action === "getPushExample") {
|
||||||
|
if (params.length <= 0) {
|
||||||
|
reject({
|
||||||
|
status: 400,
|
||||||
|
ok: false,
|
||||||
|
msg: "Missing required parameter(s)",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
socket.emit("getPushExample", params[0], (res) => {
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
status: 404,
|
||||||
|
ok: false,
|
||||||
|
msg: "Event not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
status: 401,
|
||||||
|
ok: false,
|
||||||
|
msg: "Login failed?????"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (error) => {
|
||||||
|
reject({
|
||||||
|
status: 500,
|
||||||
|
ok: false,
|
||||||
|
msg: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
reject({
|
||||||
|
status: 500,
|
||||||
|
ok: false,
|
||||||
|
msg: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Badge API
|
||||||
|
*/
|
||||||
|
|
||||||
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
||||||
allowAllOrigin(response);
|
allowAllOrigin(response);
|
||||||
|
|
||||||
|
|
11
test/api.http
Normal file
11
test/api.http
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
POST http://localhost:3001/api
|
||||||
|
Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"action": "getPushExample",
|
||||||
|
"params": [
|
||||||
|
"javascript-fetch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
###
|
Loading…
Reference in a new issue