From 6ab6d6fd3a592da1ad3a6b446701253578e2a4de Mon Sep 17 00:00:00 2001 From: Conner Hnatiuk <46903591+ConnerWithAnE@users.noreply.github.com> Date: Thu, 16 May 2024 23:26:12 -0600 Subject: [PATCH] Feature: Wg-Easy Widget (#3476) --------- Co-authored-by: ConnerWithAnE <46903591+ConnerWithAnE@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/wgeasy.md | 20 +++++++++ public/locales/en/common.json | 6 +++ src/utils/config/service-helpers.js | 6 +++ src/widgets/components.js | 1 + src/widgets/wgeasy/component.jsx | 45 +++++++++++++++++++ src/widgets/wgeasy/proxy.js | 70 +++++++++++++++++++++++++++++ src/widgets/wgeasy/widget.js | 8 ++++ src/widgets/widgets.js | 2 + 8 files changed, 158 insertions(+) create mode 100644 docs/widgets/services/wgeasy.md create mode 100644 src/widgets/wgeasy/component.jsx create mode 100644 src/widgets/wgeasy/proxy.js create mode 100644 src/widgets/wgeasy/widget.js diff --git a/docs/widgets/services/wgeasy.md b/docs/widgets/services/wgeasy.md new file mode 100644 index 000000000..c5442081c --- /dev/null +++ b/docs/widgets/services/wgeasy.md @@ -0,0 +1,20 @@ +--- +title: Wg-Easy +description: Wg-Easy Widget Configuration +--- + +Learn more about [Wg-Easy](https://github.com/wg-easy/wg-easy). + +Allowed fields: `["connected", "enabled", "disabled", "total"]`. + +Note: by default `["connected", "enabled", "total"]` are displayed. + +To detect if a device is connected the time since the last handshake is queried. `threshold` is the time to wait in minutes since the last handshake to consider a device connected. Default is 2 minutes. + +```yaml +widget: + type: wgeasy + url: http://wg.easy.or.ip + password: yourwgeasypassword + threshold: 2 # optional +``` diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 3ac3ed0d2..15de0ee94 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -876,5 +876,11 @@ "crowdsec": { "alerts": "Alerts", "bans": "Bans" + }, + "wgeasy": { + "connected": "Connected", + "enabled": "Enabled", + "disabled": "Disabled", + "total": "Total" } } diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index aaee636c9..8e2f12d54 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -462,6 +462,9 @@ export function cleanServiceGroups(groups) { // unifi site, + + // wgeasy + threshold, } = cleanedService.widget; let fieldsList = fields; @@ -596,6 +599,9 @@ export function cleanServiceGroups(groups) { cleanedService.widget.bitratePrecision = parseInt(bitratePrecision, 10); } } + if (type === "wgeasy") { + if (threshold !== undefined) cleanedService.widget.threshold = parseInt(threshold, 10); + } } return cleanedService; diff --git a/src/widgets/components.js b/src/widgets/components.js index 500fe0ce7..1b5c4b685 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -117,6 +117,7 @@ const components = { uptimerobot: dynamic(() => import("./uptimerobot/component")), urbackup: dynamic(() => import("./urbackup/component")), watchtower: dynamic(() => import("./watchtower/component")), + wgeasy: dynamic(() => import("./wgeasy/component")), whatsupdocker: dynamic(() => import("./whatsupdocker/component")), xteve: dynamic(() => import("./xteve/component")), }; diff --git a/src/widgets/wgeasy/component.jsx b/src/widgets/wgeasy/component.jsx new file mode 100644 index 000000000..0289d48c4 --- /dev/null +++ b/src/widgets/wgeasy/component.jsx @@ -0,0 +1,45 @@ +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: infoData, error: infoError } = useWidgetAPI(widget); + + if (!widget.fields) { + widget.fields = ["connected", "enabled", "total"]; + } + + if (infoError) { + return ; + } + + if (!infoData) { + return ( + + + + + + + ); + } + + const enabled = infoData.filter((item) => item.enabled).length; + const disabled = infoData.length - enabled; + const connectionThreshold = widget.threshold ?? 2 * 60 * 1000; + const currentTime = new Date(); + const connected = infoData.filter( + (item) => currentTime - new Date(item.latestHandshakeAt) < connectionThreshold, + ).length; + + return ( + + + + + + + ); +} diff --git a/src/widgets/wgeasy/proxy.js b/src/widgets/wgeasy/proxy.js new file mode 100644 index 000000000..ec733475e --- /dev/null +++ b/src/widgets/wgeasy/proxy.js @@ -0,0 +1,70 @@ +import cache from "memory-cache"; + +import getServiceWidget from "utils/config/service-helpers"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import widgets from "widgets/widgets"; +import createLogger from "utils/logger"; + +const proxyName = "wgeasyProxyHandler"; +const logger = createLogger(proxyName); +const sessionSIDCacheKey = `${proxyName}__sessionSID`; + +async function login(widget, service) { + const url = formatApiCall(widgets[widget.type].api, { ...widget, endpoint: "session" }); + const [, , , responseHeaders] = await httpProxy(url, { + method: "POST", + body: JSON.stringify({ password: widget.password }), + headers: { + "Content-Type": "application/json", + }, + }); + + try { + const connectSidCookie = responseHeaders["set-cookie"] + .find((cookie) => cookie.startsWith("connect.sid=")) + .split(";")[0] + .replace("connect.sid=", ""); + cache.put(`${sessionSIDCacheKey}.${service}`, connectSidCookie); + return connectSidCookie; + } catch (e) { + logger.error(`Error logging into wg-easy`); + cache.del(`${sessionSIDCacheKey}.${service}`); + return null; + } +} + +export default async function wgeasyProxyHandler(req, res) { + const { group, service } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (!widgets?.[widget.type]?.api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + + if (widget) { + let sid = cache.get(`${sessionSIDCacheKey}.${service}`); + if (!sid) { + sid = await login(widget, service); + if (!sid) { + return res.status(500).json({ error: "Failed to authenticate with Wg-Easy" }); + } + } + const [, , data] = await httpProxy( + formatApiCall(widgets[widget.type].api, { ...widget, endpoint: "wireguard/client" }), + { + headers: { + "Content-Type": "application/json", + Cookie: `connect.sid=${sid}`, + }, + }, + ); + + return res.json(JSON.parse(data)); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/wgeasy/widget.js b/src/widgets/wgeasy/widget.js new file mode 100644 index 000000000..7f7d69d7e --- /dev/null +++ b/src/widgets/wgeasy/widget.js @@ -0,0 +1,8 @@ +import wgeasyProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: wgeasyProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 7ed98bfb9..d6965f50f 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -107,6 +107,7 @@ import unmanic from "./unmanic/widget"; import uptimekuma from "./uptimekuma/widget"; import uptimerobot from "./uptimerobot/widget"; import watchtower from "./watchtower/widget"; +import wgeasy from "./wgeasy/widget"; import whatsupdocker from "./whatsupdocker/widget"; import xteve from "./xteve/widget"; import urbackup from "./urbackup/widget"; @@ -227,6 +228,7 @@ const widgets = { uptimerobot, urbackup, watchtower, + wgeasy, whatsupdocker, xteve, };