mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-28 01:04: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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {string} key API key to verify
|
||||
* @returns {boolean} API is ok?
|
||||
* @returns {Promise<boolean>} API is ok?
|
||||
*/
|
||||
async function verifyAPIKey(key) {
|
||||
if (typeof key !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// uk prefix + key ID is before _
|
||||
let index = key.substring(2, key.indexOf("_"));
|
||||
let clear = key.substring(key.indexOf("_") + 1, key.length);
|
||||
|
||||
const { index, clear } = exports.parseAPIKey(key);
|
||||
let hash = await R.findOne("api_key", " id=? ", [ index ]);
|
||||
|
||||
if (hash === null) {
|
||||
|
@ -65,6 +77,28 @@ async function verifyAPIKey(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 authCallback
|
||||
|
@ -80,22 +114,10 @@ async function verifyAPIKey(key) {
|
|||
* @returns {void}
|
||||
*/
|
||||
function apiAuthorizer(username, password, callback) {
|
||||
// API Rate Limit
|
||||
apiRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
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");
|
||||
verifyAPIKeyWithRateLimit(password).then(() => {
|
||||
callback(null, true);
|
||||
}).catch(() => {
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -163,3 +185,41 @@ exports.basicAuthMiddleware = async function (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 { R } = require("redbean-node");
|
||||
const apicache = require("../modules/apicache");
|
||||
|
@ -12,6 +12,10 @@ const { badgeConstants } = require("../config");
|
|||
const { Prometheus } = require("../prometheus");
|
||||
const Database = require("../database");
|
||||
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();
|
||||
|
||||
|
@ -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) => {
|
||||
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