From 32af20a67958d1ac92f6e3c3d68afb4f76e20bb6 Mon Sep 17 00:00:00 2001 From: zinsmeik <77801963+zinsmeik@users.noreply.github.com> Date: Fri, 10 May 2024 15:23:54 +0200 Subject: [PATCH] Feature: wg-easy widget --- docs/widgets/services/wgeasy.md | 20 ++++++++ public/locales/en/common.json | 6 +++ src/widgets/components.js | 1 + src/widgets/wgeasy/component.jsx | 46 ++++++++++++++++++ src/widgets/wgeasy/proxy.js | 81 ++++++++++++++++++++++++++++++++ src/widgets/wgeasy/widget.js | 8 ++++ src/widgets/widgets.js | 2 + 7 files changed, 164 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 00000000..c5442081 --- /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 3ac3ed0d..15de0ee9 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/widgets/components.js b/src/widgets/components.js index 500fe0ce..1b5c4b68 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 00000000..ecbee6d1 --- /dev/null +++ b/src/widgets/wgeasy/component.jsx @@ -0,0 +1,46 @@ +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 total = infoData.length - 1; + const enabled = infoData.filter((item) => item.enabled).length; + const disabled = total - enabled; + const connectionThreshold = infoData[infoData.length - 1].threshold * 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 00000000..25f0a8e7 --- /dev/null +++ b/src/widgets/wgeasy/proxy.js @@ -0,0 +1,81 @@ +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" }); + // eslint-disable-next-line no-unused-vars + const [status, contenType, data, responseHeaders] = await httpProxy(url, { + method: "POST", + body: JSON.stringify({ password: widget.password }), + headers: { + "Content-Type": "application/json", + }, + }); + + let connectSidCookie; + + try { + connectSidCookie = responseHeaders["set-cookie"] + .find((cookie) => cookie.startsWith("connect.sid=")) + .split(";")[0] + .replace("connect.sid=", ""); + cache.put(`${sessionSIDCacheKey}.${service}`); + } catch (e) { + logger.error(`Error logging into wg-easy`); + cache.del(`${sessionSIDCacheKey}.${service}`); + } + return [status, connectSidCookie ?? 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" }); + } + } + // eslint-disable-next-line no-unused-vars + const [status, contentType, data, responseHeaders] = await httpProxy( + formatApiCall(widgets[widget.type].api, { ...widget, endpoint: "wireguard/client" }), + { + method: "GET", + headers: { + "Content-Type": "application/json", + Cookie: `connect.sid=${sid}`, + }, + }, + ); + + const dataParsed = JSON.parse(data); + if (widget.threshold) { + dataParsed.push({ threshold: widget.threshold }); + } else { + dataParsed.push({ threshold: 2 }); + } + + return res.send(dataParsed); + } + } + + 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 00000000..7f7d69d7 --- /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 7ed98bfb..d6965f50 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, };