Compare commits

...

6 Commits

Author SHA1 Message Date
Louis Lam
49940a9dad Add jsonschema 2023-10-11 03:18:29 +08:00
Louis Lam
5fa2fcb0d9 Move port and hostname to the server object 2023-10-10 03:15:10 +08:00
Louis Lam
d4f9acee6a Add api-spec.json5 2023-10-08 08:17:03 +08:00
Louis Lam
6d2f624242 Update auth and skeleton of wrapper 2023-10-08 05:33:08 +08:00
Louis Lam
5773eeb6df Rename apiAuth to basicAuthMiddleware and it accepts API keys only 2023-10-08 02:24:18 +08:00
Louis Lam
8dfe6c6ea9 Bump socket.io from 4.6.X to 4.7.X and match the ws version with socket.io 2023-10-07 21:12:55 +08:00
9 changed files with 460 additions and 158 deletions

68
extra/api-spec.json5 Normal file
View 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
View File

@@ -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": {

View File

@@ -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",

View File

@@ -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",
});
}
};

View File

@@ -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);

View File

@@ -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();

View File

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

View File

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