mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-10 12:56:13 +08:00
Compare commits
6 Commits
2.0.0-beta
...
api-via-so
Author | SHA1 | Date | |
---|---|---|---|
|
49940a9dad | ||
|
5fa2fcb0d9 | ||
|
d4f9acee6a | ||
|
6d2f624242 | ||
|
5773eeb6df | ||
|
8dfe6c6ea9 |
68
extra/api-spec.json5
Normal file
68
extra/api-spec.json5
Normal file
@@ -0,0 +1,68 @@
|
||||
[
|
||||
{
|
||||
"name": "getPushExample",
|
||||
"description": "Get a push example.",
|
||||
"params": [
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "The programming language such as `javascript-fetch` or `python`. See the directory ./extra/push-examples for a list of available languages."
|
||||
}
|
||||
],
|
||||
"returnType": "response-json",
|
||||
"okReturn": [
|
||||
{
|
||||
"name": "code",
|
||||
"type": "string",
|
||||
"description": "The push example."
|
||||
}
|
||||
],
|
||||
"possibleErrorReasons": [
|
||||
"The parameter `language` is not available"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "checkApprise",
|
||||
"description": "Check if the apprise library is installed.",
|
||||
"params": [],
|
||||
"returnType": "boolean",
|
||||
},
|
||||
{
|
||||
"name": "getSettings",
|
||||
"description": "",
|
||||
"params": [],
|
||||
"returnType": "response-json",
|
||||
"okReturn": [
|
||||
{
|
||||
"name": "data",
|
||||
"type": "object",
|
||||
"description": "The setting object. It does not contain default values."
|
||||
}
|
||||
],
|
||||
"possibleErrorReasons": [],
|
||||
},
|
||||
{
|
||||
"name": "changePassword",
|
||||
"description": "",
|
||||
"params": [
|
||||
{
|
||||
"name": "password",
|
||||
"type": "object",
|
||||
"description": "The password object with the following properties: `currentPassword` and `newPassword`"
|
||||
}
|
||||
],
|
||||
"returnType": "response-json",
|
||||
"okReturn": [
|
||||
{
|
||||
"name": "data",
|
||||
"type": "object",
|
||||
"description": "The setting object. It does not contain default values."
|
||||
}
|
||||
],
|
||||
"possibleErrorReasons": [
|
||||
"Incorrect current password",
|
||||
"Invalid new password",
|
||||
"Password is too weak"
|
||||
],
|
||||
}
|
||||
]
|
134
package-lock.json
generated
134
package-lock.json
generated
@@ -39,7 +39,9 @@
|
||||
"iconv-lite": "~0.6.3",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"json5": "~2.2.3",
|
||||
"jsonata": "^2.0.3",
|
||||
"jsonschema": "~1.4.1",
|
||||
"jsonwebtoken": "~9.0.0",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"kafkajs": "^2.2.4",
|
||||
@@ -69,13 +71,13 @@
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"semver": "~7.5.4",
|
||||
"socket.io": "~4.6.1",
|
||||
"socket.io-client": "~4.6.1",
|
||||
"socket.io": "~4.7.2",
|
||||
"socket.io-client": "~4.7.2",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"tar": "~6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"ws": "^8.13.0"
|
||||
"ws": "~8.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~5.0.1",
|
||||
@@ -5564,9 +5566,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
|
||||
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
|
||||
"version": "2.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz",
|
||||
"integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -8730,9 +8732,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz",
|
||||
"integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.3.tgz",
|
||||
"integrity": "sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -8742,73 +8744,33 @@
|
||||
"cookie": "~0.4.1",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
|
||||
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
|
||||
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.7.tgz",
|
||||
"integrity": "sha512-P+jDFbvK6lE3n1OL+q9KuzdOFWkkZ/cMV9gol/SbVfpyqfvrfrFTOFJ6fQm2VC3PZHlU3QPhVwmbsCnauHF2MQ==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
|
||||
@@ -13246,7 +13208,6 @@
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
@@ -13274,6 +13235,14 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonschema": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
|
||||
"integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
|
||||
@@ -17015,19 +16984,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.2.tgz",
|
||||
"integrity": "sha512-Vp+lSks5k0dewYTfwgPT9UeGGd+ht7sCpB7p0e83VgO4X/AHYWhXITMrNk/pg8syY2bpx23ptClCQuHhqi2BgQ==",
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz",
|
||||
"integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.4.2",
|
||||
"engine.io": "~6.5.2",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
@@ -17038,34 +17008,14 @@
|
||||
"ws": "~8.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.2.tgz",
|
||||
"integrity": "sha512-OwWrMbbA8wSqhBAR0yoPK6EdQLERQAYjXb3A0zLpgxfM1ZGLKoxHx8gVmCHA6pcclRX5oA/zvQf7bghAS11jRA==",
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
|
||||
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.4.0",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -19255,15 +19205,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
@@ -106,7 +106,9 @@
|
||||
"iconv-lite": "~0.6.3",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"json5": "~2.2.3",
|
||||
"jsonata": "^2.0.3",
|
||||
"jsonschema": "~1.4.1",
|
||||
"jsonwebtoken": "~9.0.0",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"kafkajs": "^2.2.4",
|
||||
@@ -136,13 +138,13 @@
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"semver": "~7.5.4",
|
||||
"socket.io": "~4.6.1",
|
||||
"socket.io-client": "~4.6.1",
|
||||
"socket.io": "~4.7.2",
|
||||
"socket.io-client": "~4.7.2",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"tar": "~6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"ws": "^8.13.0"
|
||||
"ws": "~8.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~5.0.1",
|
||||
|
122
server/auth.js
122
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");
|
||||
callback(null, false);
|
||||
}
|
||||
verifyAPIKeyWithRateLimit(password).then(() => {
|
||||
callback(null, true);
|
||||
}).catch(() => {
|
||||
callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,25 +177,49 @@ exports.basicAuth = async function (req, res, next) {
|
||||
* @param {express.NextFunction} next Next handler in chain
|
||||
* @returns {void}
|
||||
*/
|
||||
exports.apiAuth = async function (req, res, next) {
|
||||
if (!await Settings.get("disableAuth")) {
|
||||
let usingAPIKeys = await Settings.get("apiKeysEnabled");
|
||||
let middleware;
|
||||
if (usingAPIKeys) {
|
||||
middleware = basicAuth({
|
||||
authorizer: apiAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
} else {
|
||||
middleware = basicAuth({
|
||||
authorizer: userAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
exports.basicAuthMiddleware = async function (req, res, next) {
|
||||
let middleware = basicAuth({
|
||||
authorizer: apiAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
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,13 @@ 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");
|
||||
const fs = require("fs");
|
||||
const JSON5 = require("json5");
|
||||
const apiSpec = JSON5.parse(fs.readFileSync("./extra/api-spec.json5", "utf8"));
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
@@ -109,6 +116,165 @@ 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;
|
||||
|
||||
let hostname = "localhost";
|
||||
if (server.hostname) {
|
||||
hostname = server.hostname;
|
||||
}
|
||||
const protocol = (server.isHTTPS) ? "wss" : "ws";
|
||||
|
||||
let wsURL = `${protocol}://${hostname}:${server.port}`;
|
||||
|
||||
const socket = ioClient(wsURL, {
|
||||
transports: [ "websocket" ],
|
||||
reconnection: false,
|
||||
});
|
||||
|
||||
try {
|
||||
let result = await socketClientHandler(socket, token, requestData);
|
||||
let status = 200;
|
||||
if (result.status) {
|
||||
status = result.status;
|
||||
} else if (typeof result === "object" && result.ok === false) {
|
||||
status = 404;
|
||||
}
|
||||
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) {
|
||||
let matched = false;
|
||||
|
||||
// Find the action in the API spec
|
||||
for (let actionObj of apiSpec) {
|
||||
|
||||
// Find it
|
||||
if (action === actionObj.name) {
|
||||
matched = true;
|
||||
let flatParams = [];
|
||||
|
||||
// Check if required parameters are provided
|
||||
if (actionObj.params.length > 0 && !params) {
|
||||
reject({
|
||||
status: 400,
|
||||
ok: false,
|
||||
msg: "Missing \"params\" property in request body",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if required parameters are valid
|
||||
for (let paramObj of actionObj.params) {
|
||||
let value = params[paramObj.name];
|
||||
|
||||
// Check if required parameter is in a correct data type
|
||||
if (typeof value !== paramObj.type) {
|
||||
reject({
|
||||
status: 400,
|
||||
ok: false,
|
||||
msg: `Parameter "${paramObj.name}" should be "${paramObj.type}". Got "${typeof value}" instead.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
flatParams.push(value);
|
||||
}
|
||||
|
||||
socket.emit(actionObj.name, ...flatParams, (res) => {
|
||||
resolve(res);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
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);
|
||||
|
||||
|
@@ -79,7 +79,7 @@ log.info("server", "Importing this project modules");
|
||||
log.debug("server", "Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
log.debug("server", "Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, FBSD, doubleCheckPassword, startE2eTests,
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleCheckPassword, startE2eTests,
|
||||
allowDevAllOrigin
|
||||
} = require("./util-server");
|
||||
|
||||
@@ -97,27 +97,13 @@ log.debug("server", "Importing Background Jobs");
|
||||
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
|
||||
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||
|
||||
const { apiAuth } = require("./auth");
|
||||
const { basicAuthMiddleware } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const checkVersion = require("./check-version");
|
||||
log.info("server", "Version: " + checkVersion.version);
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
let hostEnv = FBSD ? null : process.env.HOST;
|
||||
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
|
||||
if (hostname) {
|
||||
log.info("server", "Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
|
||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||
|
||||
@@ -182,7 +168,7 @@ let needSetup = false;
|
||||
let setupDatabase = new SetupDatabase(args, server);
|
||||
if (setupDatabase.isNeedSetup()) {
|
||||
// Hold here and start a special setup page until user choose a database type
|
||||
await setupDatabase.start(hostname, port);
|
||||
await setupDatabase.start(server.hostname, server.port);
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
@@ -267,8 +253,8 @@ let needSetup = false;
|
||||
// Basic Auth Router here
|
||||
|
||||
// Prometheus API metrics /metrics
|
||||
// With Basic Auth using the first user's username/password
|
||||
app.get("/metrics", apiAuth, prometheusAPIMetrics());
|
||||
// With Basic Auth using an API Key
|
||||
app.get("/metrics", basicAuthMiddleware, prometheusAPIMetrics());
|
||||
|
||||
app.use("/", expressStaticGzip("dist", {
|
||||
enableBrotli: true,
|
||||
@@ -1255,6 +1241,10 @@ let needSetup = false;
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (typeof password.currentPassword === "undefined") {
|
||||
throw new Error("Incorrect current password");
|
||||
}
|
||||
|
||||
if (!password.newPassword) {
|
||||
throw new Error("Invalid new password");
|
||||
}
|
||||
@@ -1745,11 +1735,11 @@ let needSetup = false;
|
||||
|
||||
server.start();
|
||||
|
||||
server.httpServer.listen(port, hostname, () => {
|
||||
if (hostname) {
|
||||
log.info("server", `Listening on ${hostname}:${port}`);
|
||||
server.httpServer.listen(server.port, server.hostname, () => {
|
||||
if (server.hostname) {
|
||||
log.info("server", `Listening on ${server.hostname}:${server.port}`);
|
||||
} else {
|
||||
log.info("server", `Listening on ${port}`);
|
||||
log.info("server", `Listening on ${server.port}`);
|
||||
}
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
|
@@ -12,6 +12,7 @@ const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
const childProcess = require("child_process");
|
||||
const path = require("path");
|
||||
const { FBSD } = require("./util-server");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||
|
||||
/**
|
||||
@@ -61,6 +62,23 @@ class UptimeKumaServer {
|
||||
*/
|
||||
jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Port
|
||||
* @type {number}
|
||||
*/
|
||||
port = undefined;
|
||||
|
||||
/**
|
||||
* Hostname
|
||||
* @type {string|undefined}
|
||||
*/
|
||||
hostname = undefined;
|
||||
|
||||
/**
|
||||
* Is SSL enabled?
|
||||
*/
|
||||
isHTTPS = false;
|
||||
|
||||
/**
|
||||
* Get the current instance of the server if it exists, otherwise
|
||||
* create a new instance.
|
||||
@@ -78,6 +96,23 @@ class UptimeKumaServer {
|
||||
* @param {object} args Arguments to initialise server with
|
||||
*/
|
||||
constructor(args) {
|
||||
|
||||
// Port
|
||||
this.port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
|
||||
// Hostname
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
let hostEnv = FBSD ? null : process.env.HOST;
|
||||
this.hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
|
||||
if (this.hostname) {
|
||||
log.info("server", "Custom hostname: " + this.hostname);
|
||||
}
|
||||
|
||||
// SSL
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
@@ -87,6 +122,7 @@ class UptimeKumaServer {
|
||||
this.app = express();
|
||||
if (sslKey && sslCert) {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.isHTTPS = true;
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert),
|
||||
@@ -94,6 +130,7 @@ class UptimeKumaServer {
|
||||
}, this.app);
|
||||
} else {
|
||||
log.info("server", "Server Type: HTTP");
|
||||
this.isHTTPS = false;
|
||||
this.httpServer = http.createServer(this.app);
|
||||
}
|
||||
|
||||
|
@@ -833,7 +833,7 @@ exports.checkLogin = (socket) => {
|
||||
*/
|
||||
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
if (typeof currentPassword !== "string") {
|
||||
throw new Error("Wrong data type?");
|
||||
throw new Error("Wrong data type of current password");
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
|
43
test/api.http
Normal file
43
test/api.http
Normal file
@@ -0,0 +1,43 @@
|
||||
POST http://localhost:3001/api
|
||||
Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "getPushExample",
|
||||
"params": {
|
||||
"language": "javascript-fetch"
|
||||
}
|
||||
}
|
||||
|
||||
###
|
||||
POST http://localhost:3001/api
|
||||
Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "checkApprise"
|
||||
}
|
||||
|
||||
###
|
||||
POST http://localhost:3001/api
|
||||
Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "getSettings"
|
||||
}
|
||||
|
||||
###
|
||||
POST http://localhost:3001/api
|
||||
Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "changePassword",
|
||||
"params": {
|
||||
"password": {
|
||||
"currentPassword": "123456",
|
||||
"newPassword": "1sfdsf234567"
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user