From 8e9ce016b118dd64bb1febb38ad5e838f7292c87 Mon Sep 17 00:00:00 2001 From: Benoit SERRA Date: Fri, 17 Feb 2023 09:25:07 +0100 Subject: [PATCH 1/2] Pterodactyl widget: initial commit --- public/locales/en/common.json | 6 +- src/widgets/components.js | 3 +- src/widgets/pterodactyl/component.jsx | 30 +++++++++ src/widgets/pterodactyl/proxy.js | 97 +++++++++++++++++++++++++++ src/widgets/pterodactyl/widget.js | 8 +++ src/widgets/widgets.js | 4 +- 6 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 src/widgets/pterodactyl/component.jsx create mode 100644 src/widgets/pterodactyl/proxy.js create mode 100644 src/widgets/pterodactyl/widget.js diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 50e796f5..6bb3377e 100755 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -517,5 +517,9 @@ "active_workers": "Active Workers", "total_workers": "Total Workers", "records_total": "Queue Length" + }, + "pterodactyl": { + "servers": "Servers", + "nodes": "Nodes" } -} \ No newline at end of file +} diff --git a/src/widgets/components.js b/src/widgets/components.js index cfd4d01a..28a70755 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -52,6 +52,7 @@ const components = { portainer: dynamic(() => import("./portainer/component")), prowlarr: dynamic(() => import("./prowlarr/component")), proxmox: dynamic(() => import("./proxmox/component")), + pterodactyl: dynamic(() => import("./pterodactyl/component")), pyload: dynamic(() => import("./pyload/component")), qbittorrent: dynamic(() => import("./qbittorrent/component")), radarr: dynamic(() => import("./radarr/component")), @@ -76,4 +77,4 @@ const components = { uptimekuma: dynamic(() => import("./uptimekuma/component")), }; -export default components; \ No newline at end of file +export default components; diff --git a/src/widgets/pterodactyl/component.jsx b/src/widgets/pterodactyl/component.jsx new file mode 100644 index 00000000..faa236ba --- /dev/null +++ b/src/widgets/pterodactyl/component.jsx @@ -0,0 +1,30 @@ + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + + const {widget} = service; + + const {data: datasData, error: datasError} = useWidgetAPI(widget); + + if (datasError) { + return ; + } + + if (!datasData) { + return ( + + + + + ); + } + return ( + + + + + ); +} diff --git a/src/widgets/pterodactyl/proxy.js b/src/widgets/pterodactyl/proxy.js new file mode 100644 index 00000000..0c36da4a --- /dev/null +++ b/src/widgets/pterodactyl/proxy.js @@ -0,0 +1,97 @@ + +import { httpProxy } from "utils/proxy/http"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; + +const proxyName = "pterodactylProxyHandler"; + +const logger = createLogger(proxyName); + +export default async function pterodactylProxyHandler(req, res) { + const { group, service } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + + const { url } = widget; + + const nodesURL = `${url}/api/application/nodes?include=servers`; + + let [status, contentType, data] = await httpProxy(nodesURL, { + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${widget.key}` + }, + }); + + if (status !== 200) { + logger.error("Unable to retrieve Pterodactyl nodes' list"); + return res.status(status).json({error: {message: `HTTP Error ${status}`, url: nodesURL, data}}); + } + + const nodesData = JSON.parse(data); + const nodesTotal = nodesData.data.length; + let nodesOnline = 0; + let total = 0; + + const serversRequests = []; + const nodesRequests = []; + + for (let nodeid = 0; nodeid < nodesData.data.length; nodeid += 1) { + // check if node is online + const nodeURL = `${nodesData.data[nodeid].attributes.scheme}://${nodesData.data[nodeid].attributes.fqdn}:${nodesData.data[nodeid].attributes.daemon_listen}/api/system`; + + nodesRequests.push(httpProxy(nodeURL)); + + for (let serverid = 0; serverid < nodesData.data[nodeid].attributes.relationships.servers.data.length; serverid += 1) { + total += 1; + const serverURL = `${url}/api/client/servers/${nodesData.data[nodeid].attributes.relationships.servers.data[serverid].attributes.identifier}/resources`; + serversRequests.push(httpProxy(serverURL, { + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${widget.key}` + }, + })); + } + } + + const nodesList = await Promise.all(nodesRequests); + + for (let nodeid = 0; nodeid < nodesList.length; nodeid += 1) { + // eslint-disable-next-line no-unused-vars + [status, contentType, data] = nodesList[nodeid]; + if (status === 401) { + nodesOnline += 1; + } + } + + let online = 0; + + const serversList = await Promise.all(serversRequests); + for (let serverid = 0; serverid < serversList.length; serverid += 1) { + // eslint-disable-next-line no-unused-vars + [status, contentType, data] = serversList[serverid]; + if (status === 200) { + const serverData = JSON.parse(data); + if (serverData.attributes.current_state === "running") { + online += 1; + } + } + } + + const servers = `${online}/${total}`; + const nodes = `${nodesOnline}/${nodesTotal}`; + + return res.send(JSON.stringify({ + nodes, + servers + })); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/pterodactyl/widget.js b/src/widgets/pterodactyl/widget.js new file mode 100644 index 00000000..63e1e499 --- /dev/null +++ b/src/widgets/pterodactyl/widget.js @@ -0,0 +1,8 @@ + +import pterodactylProxyHandler from "./proxy"; + +const widget = { + proxyHandler: pterodactylProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 7df12776..5d4b74a2 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -46,6 +46,7 @@ import plex from "./plex/widget"; import portainer from "./portainer/widget"; import prowlarr from "./prowlarr/widget"; import proxmox from "./proxmox/widget"; +import pterodactyl from "./pterodactyl/widget"; import pyload from "./pyload/widget"; import qbittorrent from "./qbittorrent/widget"; import radarr from "./radarr/widget"; @@ -119,6 +120,7 @@ const widgets = { portainer, prowlarr, proxmox, + pterodactyl, pyload, qbittorrent, radarr, @@ -144,4 +146,4 @@ const widgets = { uptimekuma, }; -export default widgets; \ No newline at end of file +export default widgets; From 060d5afcaa1f65b0a7687e2909175c70cda10c18 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 22 Feb 2023 12:43:37 -0800 Subject: [PATCH 2/2] Simplify pterodactyl to only show server / node counts --- src/utils/proxy/handlers/credentialed.js | 2 + src/widgets/pterodactyl/component.jsx | 16 ++-- src/widgets/pterodactyl/proxy.js | 97 ------------------------ src/widgets/pterodactyl/widget.js | 15 +++- 4 files changed, 24 insertions(+), 106 deletions(-) delete mode 100644 src/widgets/pterodactyl/proxy.js diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 4d5b4a25..a84897f0 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -46,6 +46,8 @@ export default async function credentialedProxyHandler(req, res, map) { } else if (widget.type === "cloudflared") { headers["X-Auth-Email"] = `${widget.email}`; headers["X-Auth-Key"] = `${widget.key}`; + } else if (widget.type === "pterodactyl") { + headers.Authorization = `Bearer ${widget.key}`; } else { headers["X-API-Key"] = `${widget.key}`; } diff --git a/src/widgets/pterodactyl/component.jsx b/src/widgets/pterodactyl/component.jsx index faa236ba..346ce234 100644 --- a/src/widgets/pterodactyl/component.jsx +++ b/src/widgets/pterodactyl/component.jsx @@ -7,13 +7,13 @@ export default function Component({ service }) { const {widget} = service; - const {data: datasData, error: datasError} = useWidgetAPI(widget); + const {data: nodesData, error: nodesError} = useWidgetAPI(widget, "nodes"); - if (datasError) { - return ; + if (nodesError) { + return ; } - if (!datasData) { + if (!nodesData) { return ( @@ -21,10 +21,14 @@ export default function Component({ service }) { ); } + + const totalServers = nodesData.data.reduce((total, node) => + node.attributes?.relationships?.servers?.data?.length ?? 0 + total, 0); + return ( - - + + ); } diff --git a/src/widgets/pterodactyl/proxy.js b/src/widgets/pterodactyl/proxy.js deleted file mode 100644 index 0c36da4a..00000000 --- a/src/widgets/pterodactyl/proxy.js +++ /dev/null @@ -1,97 +0,0 @@ - -import { httpProxy } from "utils/proxy/http"; -import getServiceWidget from "utils/config/service-helpers"; -import createLogger from "utils/logger"; - -const proxyName = "pterodactylProxyHandler"; - -const logger = createLogger(proxyName); - -export default async function pterodactylProxyHandler(req, res) { - const { group, service } = req.query; - - if (group && service) { - const widget = await getServiceWidget(group, service); - - if (widget) { - - const { url } = widget; - - const nodesURL = `${url}/api/application/nodes?include=servers`; - - let [status, contentType, data] = await httpProxy(nodesURL, { - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": `Bearer ${widget.key}` - }, - }); - - if (status !== 200) { - logger.error("Unable to retrieve Pterodactyl nodes' list"); - return res.status(status).json({error: {message: `HTTP Error ${status}`, url: nodesURL, data}}); - } - - const nodesData = JSON.parse(data); - const nodesTotal = nodesData.data.length; - let nodesOnline = 0; - let total = 0; - - const serversRequests = []; - const nodesRequests = []; - - for (let nodeid = 0; nodeid < nodesData.data.length; nodeid += 1) { - // check if node is online - const nodeURL = `${nodesData.data[nodeid].attributes.scheme}://${nodesData.data[nodeid].attributes.fqdn}:${nodesData.data[nodeid].attributes.daemon_listen}/api/system`; - - nodesRequests.push(httpProxy(nodeURL)); - - for (let serverid = 0; serverid < nodesData.data[nodeid].attributes.relationships.servers.data.length; serverid += 1) { - total += 1; - const serverURL = `${url}/api/client/servers/${nodesData.data[nodeid].attributes.relationships.servers.data[serverid].attributes.identifier}/resources`; - serversRequests.push(httpProxy(serverURL, { - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": `Bearer ${widget.key}` - }, - })); - } - } - - const nodesList = await Promise.all(nodesRequests); - - for (let nodeid = 0; nodeid < nodesList.length; nodeid += 1) { - // eslint-disable-next-line no-unused-vars - [status, contentType, data] = nodesList[nodeid]; - if (status === 401) { - nodesOnline += 1; - } - } - - let online = 0; - - const serversList = await Promise.all(serversRequests); - for (let serverid = 0; serverid < serversList.length; serverid += 1) { - // eslint-disable-next-line no-unused-vars - [status, contentType, data] = serversList[serverid]; - if (status === 200) { - const serverData = JSON.parse(data); - if (serverData.attributes.current_state === "running") { - online += 1; - } - } - } - - const servers = `${online}/${total}`; - const nodes = `${nodesOnline}/${nodesTotal}`; - - return res.send(JSON.stringify({ - nodes, - servers - })); - } - } - - return res.status(400).json({ error: "Invalid proxy service type" }); -} diff --git a/src/widgets/pterodactyl/widget.js b/src/widgets/pterodactyl/widget.js index 63e1e499..39d0eef2 100644 --- a/src/widgets/pterodactyl/widget.js +++ b/src/widgets/pterodactyl/widget.js @@ -1,8 +1,17 @@ - -import pterodactylProxyHandler from "./proxy"; +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; const widget = { - proxyHandler: pterodactylProxyHandler, + api: "{url}/api/application/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + nodes: { + endpoint: "nodes?include=servers", + validate: [ + "data" + ] + }, + }, }; export default widget;