From 2b3d4b388a62452333076e6d68fc752771dd1acb Mon Sep 17 00:00:00 2001 From: Benoit Date: Sun, 22 Jan 2023 11:51:04 +0100 Subject: [PATCH 1/4] Synology Diskstation widget : initial commit --- public/locales/en/common.json | 9 +- src/widgets/components.js | 1 + src/widgets/diskstation/component.jsx | 40 +++++++++ src/widgets/diskstation/proxy.js | 119 ++++++++++++++++++++++++++ src/widgets/diskstation/widget.js | 7 ++ src/widgets/widgets.js | 3 +- 6 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/widgets/diskstation/component.jsx create mode 100644 src/widgets/diskstation/proxy.js create mode 100644 src/widgets/diskstation/widget.js diff --git a/public/locales/en/common.json b/public/locales/en/common.json index cd3e40c4e..ba02a70f8 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -429,5 +429,12 @@ "temp_tool": "Tool temp", "temp_bed": "Bed temp", "job_completion": "Completion" + }, + "diskstation": { + "uptime": "Uptime", + "volumeUsage": "Volume Usage", + "volumeTotal": "Total size", + "cpuLoad": "CPU Load", + "memoryUsage": "Memory Usage" } -} \ No newline at end of file +} diff --git a/src/widgets/components.js b/src/widgets/components.js index 601e3f24b..fd3d39752 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -8,6 +8,7 @@ const components = { changedetectionio: dynamic(() => import("./changedetectionio/component")), coinmarketcap: dynamic(() => import("./coinmarketcap/component")), deluge: dynamic(() => import("./deluge/component")), + diskstation: dynamic(() => import("./diskstation/component")), downloadstation: dynamic(() => import("./downloadstation/component")), docker: dynamic(() => import("./docker/component")), kubernetes: dynamic(() => import("./kubernetes/component")), diff --git a/src/widgets/diskstation/component.jsx b/src/widgets/diskstation/component.jsx new file mode 100644 index 000000000..aaf69d317 --- /dev/null +++ b/src/widgets/diskstation/component.jsx @@ -0,0 +1,40 @@ +import { useTranslation } from "next-i18next"; + +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 { t } = useTranslation(); + + const { widget } = service; + + const { data: dsData, error: dsError } = useWidgetAPI(widget); + + if (dsError) { + return ; + } + + if (!dsData) { + return ( + + + + + + + + ); + } + + + return ( + + + + + + + + ); +} diff --git a/src/widgets/diskstation/proxy.js b/src/widgets/diskstation/proxy.js new file mode 100644 index 000000000..ba9f287bd --- /dev/null +++ b/src/widgets/diskstation/proxy.js @@ -0,0 +1,119 @@ + +import { formatApiCall } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import createLogger from "utils/logger"; +import getServiceWidget from "utils/config/service-helpers"; + +const proxyName = "synologyProxyHandler"; + +const logger = createLogger(proxyName); + + +function formatUptime(uptime) { + const [hour, minutes, seconds] = uptime.split(":"); + const days = Math.floor(hour/24); + const hours = hour % 24; + + return `${days} d ${hours}h${minutes}m${seconds}s` +} + +async function getApiInfo(api, widget) { + const infoAPI = "{url}/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query" + + const infoUrl = formatApiCall(infoAPI, widget); + // eslint-disable-next-line no-unused-vars + const [status, contentType, data] = await httpProxy(infoUrl); + + if (status === 200) { + const json = JSON.parse(data.toString()); + if (json.data[api]) { + const { path, minVersion, maxVersion } = json.data[api]; + return [ path, minVersion, maxVersion ]; + } + } + return [null, null, null]; +} + +async function login(widget) { + // eslint-disable-next-line no-unused-vars + const [path, minVersion, maxVersion] = await getApiInfo("SYNO.API.Auth", widget); + const authApi = `{url}/webapi/${path}?api=SYNO.API.Auth&version=${maxVersion}&method=login&account={username}&passwd={password}&format=cookie` + const loginUrl = formatApiCall(authApi, widget); + const [status, contentType, data] = await httpProxy(loginUrl); + if (status !== 200) { + return [status, contentType, data]; + } + + const json = JSON.parse(data.toString()); + + if (json?.success !== true) { + let message = "Authentication failed."; + if (json?.error?.code >= 403) message += " 2FA enabled."; + logger.warn("Unable to login. Code: %d", json?.error?.code); + return [401, "application/json", JSON.stringify({ code: json?.error?.code, message })]; + } + + return [status, contentType, data]; +} + +export default async function synologyProxyHandler(req, res) { + const { group, service } = req.query; + + if (!group || !service) { + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service); + // eslint-disable-next-line no-unused-vars + let [status, contentType, data] = await login(widget); + if (status !== 200) { + return res.status(status).end(data) + } + const { sid } = JSON.parse(data.toString()).data; + let api = "SYNO.Core.System"; + // eslint-disable-next-line no-unused-vars + let [ path, minVersion, maxVersion] = await getApiInfo(api, widget); + + const storageUrl = `${widget.url}/webapi/${path}?api=${api}&version=${maxVersion}&method=info&type="storage"&_sid=${sid}`; + + [status, contentType, data] = await httpProxy(storageUrl ); + + if (status !== 200) { + return res.status(status).set("Content-Type", contentType).send(data); + } + let json=JSON.parse(data.toString()); + if (json?.success !== true) { + return res.status(401).json({ error: "Error getting volume stats" }); + } + const totalSize = parseFloat(json.data.vol_info[0].total_size); + const usedVolume = 100 * parseFloat(json.data.vol_info[0].used_size) / parseFloat(json.data.vol_info[0].total_size); + + const healthUrl = `${widget.url}/webapi/${path}?api=${api}&version=${maxVersion}&method=info&_sid=${sid}`; + [status, contentType, data] = await httpProxy(healthUrl); + + if (status !== 200) { + return res.status(status).set("Content-Type", contentType).send(data); + } + json=JSON.parse(data.toString()); + if (json?.success !== true) { + return res.status(401).json({ error: "Error getting uptime" }); + } + const uptime = formatUptime(json.data.up_time); + api = "SYNO.Core.System.Utilization"; + // eslint-disable-next-line no-unused-vars + [ path, minVersion, maxVersion] = await getApiInfo(api, widget); + const sysUrl = `${widget.url}/webapi/${path}?api=${api}&version=${maxVersion}&method=get&_sid=${sid}`; + [status, contentType, data] = await httpProxy(sysUrl ); + + const memoryUsage = 100 - (100 * (parseFloat(JSON.parse(data.toString()).data.memory.avail_real) + parseFloat(JSON.parse(data.toString()).data.memory.cached)) / parseFloat(JSON.parse(data.toString()).data.memory.total_real)); + const cpuLoad = parseFloat(JSON.parse(data.toString()).data.cpu.user_load) + parseFloat(JSON.parse(data.toString()).data.cpu.system_load); + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(JSON.stringify({ + uptime, + usedVolume, + totalSize, + memoryUsage, + cpuLoad, + })); +} diff --git a/src/widgets/diskstation/widget.js b/src/widgets/diskstation/widget.js new file mode 100644 index 000000000..65a585867 --- /dev/null +++ b/src/widgets/diskstation/widget.js @@ -0,0 +1,7 @@ +import synologyProxyHandler from "./proxy"; + +const widget = { + proxyHandler: synologyProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 55436cec0..0b3376143 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -5,6 +5,7 @@ import bazarr from "./bazarr/widget"; import changedetectionio from "./changedetectionio/widget"; import coinmarketcap from "./coinmarketcap/widget"; import deluge from "./deluge/widget"; +import diskstation from "./diskstation/widget"; import downloadstation from "./downloadstation/widget"; import emby from "./emby/widget"; import flood from "./flood/widget"; @@ -63,7 +64,7 @@ const widgets = { changedetectionio, coinmarketcap, deluge, - diskstation: downloadstation, + diskstation, downloadstation, emby, flood, From e62952c2c1ef83034bfcbe3fc406eecc29289616 Mon Sep 17 00:00:00 2001 From: Jason Fischer Date: Sun, 29 Jan 2023 17:04:36 -0800 Subject: [PATCH 2/4] Create reusable Synology proxy - Migrate DiskStation and DownloadStation to use new proxy - Move DiskStation proxy UI logic into component --- public/locales/en/common.json | 11 +- src/utils/proxy/handlers/synology.js | 176 ++++++++++++++++++++++++++ src/widgets/diskstation/component.jsx | 43 ++++--- src/widgets/diskstation/proxy.js | 119 ----------------- src/widgets/diskstation/widget.js | 22 +++- src/widgets/downloadstation/proxy.js | 88 ------------- src/widgets/downloadstation/widget.js | 11 +- src/widgets/proxmox/component.jsx | 8 +- 8 files changed, 241 insertions(+), 237 deletions(-) create mode 100644 src/utils/proxy/handlers/synology.js delete mode 100644 src/widgets/diskstation/proxy.js delete mode 100644 src/widgets/downloadstation/proxy.js diff --git a/public/locales/en/common.json b/public/locales/en/common.json index ba02a70f8..679145692 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -32,6 +32,7 @@ }, "resources": { "cpu": "CPU", + "mem": "MEM", "total": "Total", "free": "Free", "used": "Used", @@ -431,10 +432,8 @@ "job_completion": "Completion" }, "diskstation": { - "uptime": "Uptime", - "volumeUsage": "Volume Usage", - "volumeTotal": "Total size", - "cpuLoad": "CPU Load", - "memoryUsage": "Memory Usage" + "days": "Days", + "uptime": "Uptime", + "volumeAvailable": "Available" } -} +} \ No newline at end of file diff --git a/src/utils/proxy/handlers/synology.js b/src/utils/proxy/handlers/synology.js new file mode 100644 index 000000000..3eab5df77 --- /dev/null +++ b/src/utils/proxy/handlers/synology.js @@ -0,0 +1,176 @@ +import cache from "memory-cache"; + +import getServiceWidget from "utils/config/service-helpers"; +import { asJson, formatApiCall } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import createLogger from "utils/logger"; +import widgets from "widgets/widgets"; + +const INFO_ENDPOINT = "{url}/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query"; +const AUTH_ENDPOINT = "{url}/webapi/{path}?api=SYNO.API.Auth&version={maxVersion}&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie"; +const AUTH_API_NAME = "SYNO.API.Auth"; + +const proxyName = "synologyProxyHandler"; +const logger = createLogger(proxyName); + +async function login(loginUrl) { + const [status, contentType, data] = await httpProxy(loginUrl); + if (status !== 200) { + return [status, contentType, data]; + } + + const json = asJson(data); + if (json?.success !== true) { + // from page 16: https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf + /* + Code Description + 400 No such account or incorrect password + 401 Account disabled + 402 Permission denied + 403 2-step verification code required + 404 Failed to authenticate 2-step verification code + */ + let message = "Authentication failed."; + if (json?.error?.code >= 403) message += " 2FA enabled."; + logger.warn("Unable to login. Code: %d", json?.error?.code); + return [401, "application/json", JSON.stringify({ code: json?.error?.code, message })]; + } + + return [status, contentType, data]; +} + +async function getApiInfo(serviceWidget, apiName) { + const cacheKey = `${proxyName}__${apiName}`; + let { cgiPath, maxVersion } = cache.get(cacheKey) ?? {}; + if (cgiPath && maxVersion) { + return [cgiPath, maxVersion]; + } + + const infoUrl = formatApiCall(INFO_ENDPOINT, serviceWidget); + // eslint-disable-next-line no-unused-vars + const [status, contentType, data] = await httpProxy(infoUrl); + + if (status === 200) { + try { + const json = asJson(data); + if (json?.data?.[apiName]) { + cgiPath = json.data[apiName].path; + maxVersion = json.data[apiName].maxVersion; + logger.debug(`Detected ${serviceWidget.type}: apiName '${apiName}', cgiPath '${cgiPath}', and maxVersion ${maxVersion}`); + cache.put(cacheKey, { cgiPath, maxVersion }); + return [cgiPath, maxVersion]; + } + } + catch { + logger.warn(`Error ${status} obtaining ${apiName} info`); + } + } + + return [null, null]; +} + +async function handleUnsuccessfulResponse(serviceWidget, url) { + logger.debug(`Attempting login to ${serviceWidget.type}`); + + // eslint-disable-next-line no-unused-vars + const [apiPath, maxVersion] = await getApiInfo(serviceWidget, AUTH_API_NAME); + + const authArgs = { path: apiPath ?? "entry.cgi", maxVersion: maxVersion ?? 7, ...serviceWidget }; + const loginUrl = formatApiCall(AUTH_ENDPOINT, authArgs); + + const [status, contentType, data] = await login(loginUrl); + if (status !== 200) { + return [status, contentType, data]; + } + + return httpProxy(url); +} + +function toError(url, synologyError) { + // commeon codes (100 => 199) from: + // https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf + const code = synologyError.error?.code ?? synologyError.error ?? synologyError.code ?? 100; + const error = { code }; + switch (code) { + case 102: + error.error = "The requested API does not exist."; + break; + + case 103: + error.error = "The requested method does not exist."; + break; + + case 104: + error.error = "The requested version does not support the functionality."; + break; + + case 105: + error.error = "The logged in session does not have permission."; + break; + + case 106: + error.error = "Session timeout."; + break; + + case 107: + error.error = "Session interrupted by duplicated login."; + break; + + case 119: + error.error = "Invalid session or SID not found."; + break; + + default: + error.error = synologyError.message ?? "Unknown error."; + break; + } + logger.warn(`Unable to call ${url}. code: ${code}, error: ${error.error}.`) + return error; +} + +export default async function synologyProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (!group || !service) { + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const serviceWidget = await getServiceWidget(group, service); + const widget = widgets?.[serviceWidget.type]; + const mapping = widget?.mappings?.[endpoint]; + if (!widget.api || !mapping) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + + const [cgiPath, maxVersion] = await getApiInfo(serviceWidget, mapping.apiName); + if (!cgiPath || !maxVersion) { + return res.status(400).json({ error: `Unrecognized API name: ${mapping.apiName}`}) + } + + const url = formatApiCall(widget.api, { + apiName: mapping.apiName, + apiMethod: mapping.apiMethod, + cgiPath, + maxVersion, + ...serviceWidget + }); + let [status, contentType, data] = await httpProxy(url); + if (status !== 200) { + logger.debug("Error %d calling url %s", status, url); + return res.status(status, data); + } + + let json = asJson(data); + if (json?.success !== true) { + logger.debug(`Attempting login to ${serviceWidget.type}`); + [status, contentType, data] = await handleUnsuccessfulResponse(serviceWidget, url); + json = asJson(data); + } + + if (json.success !== true) { + data = toError(url, json); + status = 500; + } + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(data); +} diff --git a/src/widgets/diskstation/component.jsx b/src/widgets/diskstation/component.jsx index aaf69d317..ea5d9b35c 100644 --- a/src/widgets/diskstation/component.jsx +++ b/src/widgets/diskstation/component.jsx @@ -6,35 +6,48 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { const { t } = useTranslation(); - const { widget } = service; + const { data: infoData, error: infoError } = useWidgetAPI(widget, "system_info"); + const { data: storageData, error: storageError } = useWidgetAPI(widget, "system_storage"); + const { data: utilizationData, error: utilizationError } = useWidgetAPI(widget, "utilization"); - const { data: dsData, error: dsError } = useWidgetAPI(widget); - - if (dsError) { - return ; + if (storageError || infoError || utilizationError) { + return ; } - if (!dsData) { + if (!storageData || !infoData || !utilizationData) { return ( - - - - + + + ); } + // uptime info + // eslint-disable-next-line no-unused-vars + const [hour, minutes, seconds] = infoData.data.up_time.split(":"); + const days = Math.floor(hour / 24); + const uptime = `${ t("common.number", { value: days }) } ${ t("diskstation.days") }`; + + // storage info + // TODO: figure out how to display info for more than one volume + const volume = storageData.data.vol_info?.[0]; + const freeVolume = 100 - (100 * (parseFloat(volume?.used_size) / parseFloat(volume?.total_size))); + + // utilization info + const { cpu, memory } = utilizationData.data; + const cpuLoad = parseFloat(cpu.user_load) + parseFloat(cpu.system_load); + const memoryUsage = 100 - ((100 * (parseFloat(memory.avail_real) + parseFloat(memory.cached))) / parseFloat(memory.total_real)); return ( - - - - - + + + + ); } diff --git a/src/widgets/diskstation/proxy.js b/src/widgets/diskstation/proxy.js deleted file mode 100644 index ba9f287bd..000000000 --- a/src/widgets/diskstation/proxy.js +++ /dev/null @@ -1,119 +0,0 @@ - -import { formatApiCall } from "utils/proxy/api-helpers"; -import { httpProxy } from "utils/proxy/http"; -import createLogger from "utils/logger"; -import getServiceWidget from "utils/config/service-helpers"; - -const proxyName = "synologyProxyHandler"; - -const logger = createLogger(proxyName); - - -function formatUptime(uptime) { - const [hour, minutes, seconds] = uptime.split(":"); - const days = Math.floor(hour/24); - const hours = hour % 24; - - return `${days} d ${hours}h${minutes}m${seconds}s` -} - -async function getApiInfo(api, widget) { - const infoAPI = "{url}/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query" - - const infoUrl = formatApiCall(infoAPI, widget); - // eslint-disable-next-line no-unused-vars - const [status, contentType, data] = await httpProxy(infoUrl); - - if (status === 200) { - const json = JSON.parse(data.toString()); - if (json.data[api]) { - const { path, minVersion, maxVersion } = json.data[api]; - return [ path, minVersion, maxVersion ]; - } - } - return [null, null, null]; -} - -async function login(widget) { - // eslint-disable-next-line no-unused-vars - const [path, minVersion, maxVersion] = await getApiInfo("SYNO.API.Auth", widget); - const authApi = `{url}/webapi/${path}?api=SYNO.API.Auth&version=${maxVersion}&method=login&account={username}&passwd={password}&format=cookie` - const loginUrl = formatApiCall(authApi, widget); - const [status, contentType, data] = await httpProxy(loginUrl); - if (status !== 200) { - return [status, contentType, data]; - } - - const json = JSON.parse(data.toString()); - - if (json?.success !== true) { - let message = "Authentication failed."; - if (json?.error?.code >= 403) message += " 2FA enabled."; - logger.warn("Unable to login. Code: %d", json?.error?.code); - return [401, "application/json", JSON.stringify({ code: json?.error?.code, message })]; - } - - return [status, contentType, data]; -} - -export default async function synologyProxyHandler(req, res) { - const { group, service } = req.query; - - if (!group || !service) { - return res.status(400).json({ error: "Invalid proxy service type" }); - } - - const widget = await getServiceWidget(group, service); - // eslint-disable-next-line no-unused-vars - let [status, contentType, data] = await login(widget); - if (status !== 200) { - return res.status(status).end(data) - } - const { sid } = JSON.parse(data.toString()).data; - let api = "SYNO.Core.System"; - // eslint-disable-next-line no-unused-vars - let [ path, minVersion, maxVersion] = await getApiInfo(api, widget); - - const storageUrl = `${widget.url}/webapi/${path}?api=${api}&version=${maxVersion}&method=info&type="storage"&_sid=${sid}`; - - [status, contentType, data] = await httpProxy(storageUrl ); - - if (status !== 200) { - return res.status(status).set("Content-Type", contentType).send(data); - } - let json=JSON.parse(data.toString()); - if (json?.success !== true) { - return res.status(401).json({ error: "Error getting volume stats" }); - } - const totalSize = parseFloat(json.data.vol_info[0].total_size); - const usedVolume = 100 * parseFloat(json.data.vol_info[0].used_size) / parseFloat(json.data.vol_info[0].total_size); - - const healthUrl = `${widget.url}/webapi/${path}?api=${api}&version=${maxVersion}&method=info&_sid=${sid}`; - [status, contentType, data] = await httpProxy(healthUrl); - - if (status !== 200) { - return res.status(status).set("Content-Type", contentType).send(data); - } - json=JSON.parse(data.toString()); - if (json?.success !== true) { - return res.status(401).json({ error: "Error getting uptime" }); - } - const uptime = formatUptime(json.data.up_time); - api = "SYNO.Core.System.Utilization"; - // eslint-disable-next-line no-unused-vars - [ path, minVersion, maxVersion] = await getApiInfo(api, widget); - const sysUrl = `${widget.url}/webapi/${path}?api=${api}&version=${maxVersion}&method=get&_sid=${sid}`; - [status, contentType, data] = await httpProxy(sysUrl ); - - const memoryUsage = 100 - (100 * (parseFloat(JSON.parse(data.toString()).data.memory.avail_real) + parseFloat(JSON.parse(data.toString()).data.memory.cached)) / parseFloat(JSON.parse(data.toString()).data.memory.total_real)); - const cpuLoad = parseFloat(JSON.parse(data.toString()).data.cpu.user_load) + parseFloat(JSON.parse(data.toString()).data.cpu.system_load); - - if (contentType) res.setHeader("Content-Type", contentType); - return res.status(status).send(JSON.stringify({ - uptime, - usedVolume, - totalSize, - memoryUsage, - cpuLoad, - })); -} diff --git a/src/widgets/diskstation/widget.js b/src/widgets/diskstation/widget.js index 65a585867..6cb8971e0 100644 --- a/src/widgets/diskstation/widget.js +++ b/src/widgets/diskstation/widget.js @@ -1,7 +1,27 @@ -import synologyProxyHandler from "./proxy"; +import synologyProxyHandler from '../../utils/proxy/handlers/synology' const widget = { + // cgiPath and maxVersion are discovered at runtime, don't supply + api: "{url}/webapi/{cgiPath}?api={apiName}&version={maxVersion}&method={apiMethod}", proxyHandler: synologyProxyHandler, + + mappings: { + "system_storage": { + apiName: "SYNO.Core.System", + apiMethod: "info&type=\"storage\"", + endpoint: "system_storage" + }, + "system_info": { + apiName: "SYNO.Core.System", + apiMethod: "info", + endpoint: "system_info" + }, + "utilization": { + apiName: "SYNO.Core.System.Utilization", + apiMethod: "get", + endpoint: "utilization" + } + }, }; export default widget; diff --git a/src/widgets/downloadstation/proxy.js b/src/widgets/downloadstation/proxy.js deleted file mode 100644 index 73a6a259e..000000000 --- a/src/widgets/downloadstation/proxy.js +++ /dev/null @@ -1,88 +0,0 @@ -import { formatApiCall } from "utils/proxy/api-helpers"; -import { httpProxy } from "utils/proxy/http"; -import createLogger from "utils/logger"; -import widgets from "widgets/widgets"; -import getServiceWidget from "utils/config/service-helpers"; - -const logger = createLogger("downloadstationProxyHandler"); - -async function login(loginUrl) { - const [status, contentType, data] = await httpProxy(loginUrl); - if (status !== 200) { - return [status, contentType, data]; - } - - const json = JSON.parse(data.toString()); - if (json?.success !== true) { - // from https://global.download.synology.com/download/Document/Software/DeveloperGuide/Package/DownloadStation/All/enu/Synology_Download_Station_Web_API.pdf - /* - Code Description - 400 No such account or incorrect password - 401 Account disabled - 402 Permission denied - 403 2-step verification code required - 404 Failed to authenticate 2-step verification code - */ - let message = "Authentication failed."; - if (json?.error?.code >= 403) message += " 2FA enabled."; - logger.warn("Unable to login. Code: %d", json?.error?.code); - return [401, "application/json", JSON.stringify({ code: json?.error?.code, message })]; - } - - return [status, contentType, data]; -} - -export default async function downloadstationProxyHandler(req, res) { - const { group, service, endpoint } = req.query; - - if (!group || !service) { - return res.status(400).json({ error: "Invalid proxy service type" }); - } - - const widget = await getServiceWidget(group, service); - const api = widgets?.[widget.type]?.api; - if (!api) { - return res.status(403).json({ error: "Service does not support API calls" }); - } - - const url = formatApiCall(api, { endpoint, ...widget }); - let [status, contentType, data] = await httpProxy(url); - if (status !== 200) { - logger.debug("Error %d calling endpoint %s", status, url); - return res.status(status, data); - } - - const json = JSON.parse(data.toString()); - if (json?.success !== true) { - logger.debug("Attempting login to DownloadStation"); - - const apiInfoUrl = formatApiCall("{url}/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query", widget); - let path = "entry.cgi"; - let maxVersion = 7; - [status, contentType, data] = await httpProxy(apiInfoUrl); - if (status === 200) { - try { - const apiAuthInfo = JSON.parse(data.toString()).data['SYNO.API.Auth']; - if (apiAuthInfo) { - path = apiAuthInfo.path; - maxVersion = apiAuthInfo.maxVersion; - logger.debug(`Deteceted Downloadstation auth API path: ${path} and maxVersion: ${maxVersion}`); - } - } catch { - logger.debug(`Error ${status} obtaining DownloadStation API info`); - } - } - - const authApi = `{url}/webapi/${path}?api=SYNO.API.Auth&version=${maxVersion}&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie` - const loginUrl = formatApiCall(authApi, widget); - [status, contentType, data] = await login(loginUrl); - if (status !== 200) { - return res.status(status).end(data) - } - - [status, contentType, data] = await httpProxy(url); - } - - if (contentType) res.setHeader("Content-Type", contentType); - return res.status(status).send(data); -} diff --git a/src/widgets/downloadstation/widget.js b/src/widgets/downloadstation/widget.js index 38245adfd..02895723f 100644 --- a/src/widgets/downloadstation/widget.js +++ b/src/widgets/downloadstation/widget.js @@ -1,12 +1,15 @@ -import downloadstationProxyHandler from "./proxy"; +import synologyProxyHandler from '../../utils/proxy/handlers/synology' const widget = { - api: "{url}/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method={endpoint}", - proxyHandler: downloadstationProxyHandler, + // cgiPath and maxVersion are discovered at runtime, don't supply + api: "{url}/webapi/{cgiPath}?api={apiName}&version={maxVersion}&method={apiMethod}", + proxyHandler: synologyProxyHandler, mappings: { "list": { - endpoint: "list&additional=transfer", + apiName: "SYNO.DownloadStation.Task", + apiMethod: "list&additional=transfer", + endpoint: "list" }, }, }; diff --git a/src/widgets/proxmox/component.jsx b/src/widgets/proxmox/component.jsx index 1d384b546..ac443a341 100644 --- a/src/widgets/proxmox/component.jsx +++ b/src/widgets/proxmox/component.jsx @@ -24,8 +24,8 @@ export default function Component({ service }) { - - + + ); } @@ -46,8 +46,8 @@ export default function Component({ service }) { - - + + ); } From 4c17e3fd143714c8ad1acf3b6051145a3ae43a4c Mon Sep 17 00:00:00 2001 From: Benoit Date: Tue, 31 Jan 2023 12:07:23 +0100 Subject: [PATCH 3/4] Synology Widget : making it work with multiple widgets connecting to differents versions of DSM --- src/utils/proxy/handlers/synology.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/proxy/handlers/synology.js b/src/utils/proxy/handlers/synology.js index 3eab5df77..9895e113e 100644 --- a/src/utils/proxy/handlers/synology.js +++ b/src/utils/proxy/handlers/synology.js @@ -39,8 +39,8 @@ async function login(loginUrl) { return [status, contentType, data]; } -async function getApiInfo(serviceWidget, apiName) { - const cacheKey = `${proxyName}__${apiName}`; +async function getApiInfo(serviceWidget, apiName, serviceName) { + const cacheKey = `${proxyName}__${apiName}__${serviceName}` let { cgiPath, maxVersion } = cache.get(cacheKey) ?? {}; if (cgiPath && maxVersion) { return [cgiPath, maxVersion]; @@ -69,15 +69,15 @@ async function getApiInfo(serviceWidget, apiName) { return [null, null]; } -async function handleUnsuccessfulResponse(serviceWidget, url) { +async function handleUnsuccessfulResponse(serviceWidget, url, serviceName) { logger.debug(`Attempting login to ${serviceWidget.type}`); // eslint-disable-next-line no-unused-vars - const [apiPath, maxVersion] = await getApiInfo(serviceWidget, AUTH_API_NAME); + const [apiPath, maxVersion] = await getApiInfo(serviceWidget, AUTH_API_NAME, serviceName); const authArgs = { path: apiPath ?? "entry.cgi", maxVersion: maxVersion ?? 7, ...serviceWidget }; const loginUrl = formatApiCall(AUTH_ENDPOINT, authArgs); - + const [status, contentType, data] = await login(loginUrl); if (status !== 200) { return [status, contentType, data]; @@ -142,7 +142,7 @@ export default async function synologyProxyHandler(req, res) { return res.status(403).json({ error: "Service does not support API calls" }); } - const [cgiPath, maxVersion] = await getApiInfo(serviceWidget, mapping.apiName); + const [cgiPath, maxVersion] = await getApiInfo(serviceWidget, mapping.apiName, service); if (!cgiPath || !maxVersion) { return res.status(400).json({ error: `Unrecognized API name: ${mapping.apiName}`}) } @@ -163,7 +163,7 @@ export default async function synologyProxyHandler(req, res) { let json = asJson(data); if (json?.success !== true) { logger.debug(`Attempting login to ${serviceWidget.type}`); - [status, contentType, data] = await handleUnsuccessfulResponse(serviceWidget, url); + [status, contentType, data] = await handleUnsuccessfulResponse(serviceWidget, url, service); json = asJson(data); } From f53f97566920716ec7a0dbf44cf3dd20ee93b210 Mon Sep 17 00:00:00 2001 From: Jason Fischer Date: Mon, 6 Feb 2023 13:43:40 -0800 Subject: [PATCH 4/4] Settle on four fields with free size displayed as Available --- src/components/services/widget/container.jsx | 13 ++++++++++++- src/widgets/diskstation/component.jsx | 6 ++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx index 945b8f6f1..11dd4f563 100644 --- a/src/components/services/widget/container.jsx +++ b/src/components/services/widget/container.jsx @@ -9,7 +9,18 @@ export default function Container({ error = false, children, service }) { const fields = service?.widget?.fields; const type = service?.widget?.type; if (fields && type) { - visibleChildren = children.filter(child => fields.some(field => `${type}.${field}` === child?.props?.label)); + // if the field contains a "." then it most likely contains a common loc value + // logic now allows a fields array that can look like: + // fields: [ "resources.cpu", "resources.mem", "field"] + // or even + // fields: [ "resources.cpu", "widget_type.field" ] + visibleChildren = children.filter(child => fields.some(field => { + let fullField = field; + if (!field.includes(".")) { + fullField = `${type}.${field}`; + } + return fullField === child?.props?.label; + })); } return
{visibleChildren}
; diff --git a/src/widgets/diskstation/component.jsx b/src/widgets/diskstation/component.jsx index ea5d9b35c..3195a62dd 100644 --- a/src/widgets/diskstation/component.jsx +++ b/src/widgets/diskstation/component.jsx @@ -35,7 +35,9 @@ export default function Component({ service }) { // storage info // TODO: figure out how to display info for more than one volume const volume = storageData.data.vol_info?.[0]; - const freeVolume = 100 - (100 * (parseFloat(volume?.used_size) / parseFloat(volume?.total_size))); + const usedBytes = parseFloat(volume?.used_size); + const totalBytes = parseFloat(volume?.total_size); + const freeBytes = totalBytes - usedBytes; // utilization info const { cpu, memory } = utilizationData.data; @@ -45,7 +47,7 @@ export default function Component({ service }) { return ( - +