diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
index be4a96a67..9347c4eb2 100644
--- a/src/pages/api/services/proxy.js
+++ b/src/pages/api/services/proxy.js
@@ -18,6 +18,11 @@ export default async function handler(req, res) {
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
if (serviceProxyHandler instanceof Function) {
+ // quick return for no endpoint services
+ if (!req.query.endpoint) {
+ return serviceProxyHandler(req, res);
+ }
+
// map opaque endpoints to their actual endpoint
if (widget?.mappings) {
const mapping = widget?.mappings?.[req.query.endpoint];
@@ -38,6 +43,15 @@ export default async function handler(req, res) {
if (req.query.segments) {
const segments = JSON.parse(req.query.segments);
+ for (const key in segments) {
+ if (!mapping.segments.includes(key)) {
+ logger.debug("Unsupported segment: %s", key);
+ return res.status(403).json({ error: "Unsupported segment" });
+ } else if (segments[key].includes("/")) {
+ logger.debug("Unsupported segment value: %s", segments[key]);
+ return res.status(403).json({ error: "Unsupported segment value" });
+ }
+ }
req.query.endpoint = formatApiCall(endpoint, segments);
}
@@ -66,7 +80,14 @@ export default async function handler(req, res) {
return serviceProxyHandler(req, res, map);
}
- return serviceProxyHandler(req, res);
+ if (widget.allowedEndpoints instanceof RegExp) {
+ if (widget.allowedEndpoints.test(req.query.endpoint)) {
+ return serviceProxyHandler(req, res);
+ }
+ }
+
+ logger.debug("Unmapped proxy request.");
+ return res.status(403).json({ error: "Unmapped proxy request." });
}
logger.debug("Unknown proxy service type: %s", type);
diff --git a/src/utils/proxy/api-helpers.js b/src/utils/proxy/api-helpers.js
index ffd2f63bc..dceea3c4f 100644
--- a/src/utils/proxy/api-helpers.js
+++ b/src/utils/proxy/api-helpers.js
@@ -8,22 +8,16 @@ export function formatApiCall(url, args) {
return url.replace(/\/+$/, "").replace(find, replace).replace(find, replace);
}
-function getURLSearchParams(widget, endpoint) {
+export function getURLSearchParams(widget, endpoint) {
const params = new URLSearchParams({
type: widget.type,
group: widget.service_group,
service: widget.service_name,
- endpoint,
});
- return params;
-}
-
-export function formatProxyUrlWithSegments(widget, endpoint, segments) {
- const params = getURLSearchParams(widget, endpoint);
- if (segments) {
- params.append("segments", JSON.stringify(segments));
+ if (endpoint) {
+ params.append("endpoint", endpoint);
}
- return `/api/services/proxy?${params.toString()}`;
+ return params;
}
export function formatProxyUrl(widget, endpoint, queryParams) {
@@ -59,6 +53,7 @@ export function sanitizeErrorURL(errorURL) {
const url = new URL(errorURL);
["apikey", "api_key", "token", "t", "access_token", "auth"].forEach((key) => {
if (url.searchParams.has(key)) url.searchParams.set(key, "***");
+ if (url.hash.includes(key)) url.hash = url.hash.replace(new RegExp(`${key}=[^&]+`), `${key}=***`);
});
return url.toString();
}
diff --git a/src/widgets/emby/component.jsx b/src/widgets/emby/component.jsx
index 9084cbac2..090a9c3f4 100644
--- a/src/widgets/emby/component.jsx
+++ b/src/widgets/emby/component.jsx
@@ -4,7 +4,7 @@ import { MdOutlineSmartDisplay } from "react-icons/md";
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-import { formatProxyUrlWithSegments } from "utils/proxy/api-helpers";
+import { getURLSearchParams } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
function ticksToTime(ticks) {
@@ -217,10 +217,14 @@ export default function Component({ service }) {
});
async function handlePlayCommand(session, command) {
- const url = formatProxyUrlWithSegments(widget, "PlayControl", {
- sessionId: session.Id,
- command,
- });
+ const params = getURLSearchParams(widget, command);
+ params.append(
+ "segments",
+ JSON.stringify({
+ sessionId: session.Id,
+ }),
+ );
+ const url = `/api/services/proxy?${params.toString()}`;
await fetch(url).then(() => {
sessionMutate();
});
diff --git a/src/widgets/emby/widget.js b/src/widgets/emby/widget.js
index 1dc009b2a..3b04f59fb 100644
--- a/src/widgets/emby/widget.js
+++ b/src/widgets/emby/widget.js
@@ -10,12 +10,16 @@ const widget = {
},
Count: {
endpoint: "Items/Counts",
- segments: ["MovieCount", "SeriesCount", "EpisodeCount", "SongCount"],
},
- PlayControl: {
+ Unpause: {
method: "POST",
- endpoint: "Sessions/{sessionId}/Playing/{command}",
- segments: ["sessionId", "command"],
+ endpoint: "Sessions/{sessionId}/Playing/Unpause",
+ segments: ["sessionId"],
+ },
+ Pause: {
+ method: "POST",
+ endpoint: "Sessions/{sessionId}/Playing/Pause",
+ segments: ["sessionId"],
},
},
};
diff --git a/src/widgets/flood/widget.js b/src/widgets/flood/widget.js
index 027ff344b..13413cf44 100644
--- a/src/widgets/flood/widget.js
+++ b/src/widgets/flood/widget.js
@@ -2,6 +2,12 @@ import floodProxyHandler from "./proxy";
const widget = {
proxyHandler: floodProxyHandler,
+
+ mappings: {
+ torrents: {
+ endpoint: "torrents",
+ },
+ },
};
export default widget;
diff --git a/src/widgets/fritzbox/widget.js b/src/widgets/fritzbox/widget.js
index 131938210..32e8a5c27 100644
--- a/src/widgets/fritzbox/widget.js
+++ b/src/widgets/fritzbox/widget.js
@@ -2,6 +2,7 @@ import fritzboxProxyHandler from "./proxy";
const widget = {
proxyHandler: fritzboxProxyHandler,
+ allowedEndpoints: /status/,
};
export default widget;
diff --git a/src/widgets/gamedig/widget.js b/src/widgets/gamedig/widget.js
index 6ccfa123a..0f888b43d 100644
--- a/src/widgets/gamedig/widget.js
+++ b/src/widgets/gamedig/widget.js
@@ -2,6 +2,7 @@ import gamedigProxyHandler from "./proxy";
const widget = {
proxyHandler: gamedigProxyHandler,
+ allowedEndpoints: /status/,
};
export default widget;
diff --git a/src/widgets/glances/widget.js b/src/widgets/glances/widget.js
index 3357cf28e..e018ae39b 100644
--- a/src/widgets/glances/widget.js
+++ b/src/widgets/glances/widget.js
@@ -3,6 +3,7 @@ import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: credentialedProxyHandler,
+ allowedEndpoints: /\d\/quicklook|diskio|fs|gpu|system|mem|network|processlist|sensors/,
};
export default widget;
diff --git a/src/widgets/minecraft/widget.js b/src/widgets/minecraft/widget.js
index f8a81bfb2..fbe413b75 100644
--- a/src/widgets/minecraft/widget.js
+++ b/src/widgets/minecraft/widget.js
@@ -2,6 +2,7 @@ import minecraftProxyHandler from "./proxy";
const widget = {
proxyHandler: minecraftProxyHandler,
+ allowedEndpoints: /status/,
};
export default widget;
diff --git a/src/widgets/npm/component.jsx b/src/widgets/npm/component.jsx
index 377122664..06ac91ebe 100644
--- a/src/widgets/npm/component.jsx
+++ b/src/widgets/npm/component.jsx
@@ -5,7 +5,7 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { widget } = service;
- const { data: infoData, error: infoError } = useWidgetAPI(widget, "nginx/proxy-hosts");
+ const { data: infoData, error: infoError } = useWidgetAPI(widget, "hosts");
if (infoError) {
return ;
diff --git a/src/widgets/npm/widget.js b/src/widgets/npm/widget.js
index 652cb4a25..24b3ce029 100644
--- a/src/widgets/npm/widget.js
+++ b/src/widgets/npm/widget.js
@@ -3,6 +3,12 @@ import npmProxyHandler from "./proxy";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: npmProxyHandler,
+
+ mappings: {
+ hosts: {
+ endpoint: "nginx/proxy-hosts",
+ },
+ },
};
export default widget;
diff --git a/src/widgets/nzbget/widget.js b/src/widgets/nzbget/widget.js
index 841fb66c0..79ca1807d 100644
--- a/src/widgets/nzbget/widget.js
+++ b/src/widgets/nzbget/widget.js
@@ -3,6 +3,7 @@ import jsonrpcProxyHandler from "utils/proxy/handlers/jsonrpc";
const widget = {
api: "{url}/jsonrpc",
proxyHandler: jsonrpcProxyHandler,
+ allowedEndpoints: /status/,
};
export default widget;
diff --git a/src/widgets/qbittorrent/component.jsx b/src/widgets/qbittorrent/component.jsx
index 615709ea6..e88b26227 100644
--- a/src/widgets/qbittorrent/component.jsx
+++ b/src/widgets/qbittorrent/component.jsx
@@ -9,7 +9,7 @@ export default function Component({ service }) {
const { widget } = service;
- const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents/info");
+ const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents");
if (torrentError) {
return ;
diff --git a/src/widgets/qbittorrent/widget.js b/src/widgets/qbittorrent/widget.js
index 1e8348b33..182ac9d1b 100644
--- a/src/widgets/qbittorrent/widget.js
+++ b/src/widgets/qbittorrent/widget.js
@@ -2,6 +2,12 @@ import qbittorrentProxyHandler from "./proxy";
const widget = {
proxyHandler: qbittorrentProxyHandler,
+
+ mappings: {
+ torrents: {
+ endpoint: "torrents/info",
+ },
+ },
};
export default widget;
diff --git a/src/widgets/qnap/widget.js b/src/widgets/qnap/widget.js
index ebaf93c9d..1069fa9a4 100644
--- a/src/widgets/qnap/widget.js
+++ b/src/widgets/qnap/widget.js
@@ -3,6 +3,7 @@ import qnapProxyHandler from "./proxy";
const widget = {
api: "{url}",
proxyHandler: qnapProxyHandler,
+ allowedEndpoints: /status/,
};
export default widget;
diff --git a/src/widgets/swagdashboard/widget.js b/src/widgets/swagdashboard/widget.js
index 626586fe1..7067e55d2 100644
--- a/src/widgets/swagdashboard/widget.js
+++ b/src/widgets/swagdashboard/widget.js
@@ -3,6 +3,7 @@ import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/?stats=true",
proxyHandler: genericProxyHandler,
+ allowedEndpoints: /overview/,
};
export default widget;
diff --git a/src/widgets/tdarr/proxy.js b/src/widgets/tdarr/proxy.js
index a1ebc149b..898082f40 100644
--- a/src/widgets/tdarr/proxy.js
+++ b/src/widgets/tdarr/proxy.js
@@ -8,7 +8,7 @@ const proxyName = "tdarrProxyHandler";
const logger = createLogger(proxyName);
export default async function tdarrProxyHandler(req, res) {
- const { group, service, endpoint } = req.query;
+ const { group, service } = req.query;
if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
@@ -22,7 +22,7 @@ export default async function tdarrProxyHandler(req, res) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
- const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
+ const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint: undefined, ...widget }));
const [status, contentType, data] = await httpProxy(url, {
method: "POST",
diff --git a/src/widgets/transmission/proxy.js b/src/widgets/transmission/proxy.js
index f12d2a0c0..823def054 100644
--- a/src/widgets/transmission/proxy.js
+++ b/src/widgets/transmission/proxy.js
@@ -11,7 +11,7 @@ const headerCacheKey = `${proxyName}__headers`;
const logger = createLogger(proxyName);
export default async function transmissionProxyHandler(req, res) {
- const { group, service, endpoint } = req.query;
+ const { group, service } = req.query;
if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
@@ -35,7 +35,7 @@ export default async function transmissionProxyHandler(req, res) {
const api = `${widget.url}${widget.rpcUrl || widgets[widget.type].rpcUrl}rpc`;
- const url = new URL(formatApiCall(api, { endpoint, ...widget }));
+ const url = new URL(formatApiCall(api, { endpoint: undefined, ...widget }));
const csrfHeaderName = "x-transmission-session-id";
const method = "POST";
diff --git a/src/widgets/urbackup/widget.js b/src/widgets/urbackup/widget.js
index 5eac66d07..96c52296e 100644
--- a/src/widgets/urbackup/widget.js
+++ b/src/widgets/urbackup/widget.js
@@ -2,6 +2,7 @@ import urbackupProxyHandler from "./proxy";
const widget = {
proxyHandler: urbackupProxyHandler,
+ allowedEndpoints: /status/,
};
export default widget;
diff --git a/src/widgets/xteve/component.jsx b/src/widgets/xteve/component.jsx
index 75629909a..84a617c2e 100644
--- a/src/widgets/xteve/component.jsx
+++ b/src/widgets/xteve/component.jsx
@@ -9,7 +9,7 @@ export default function Component({ service }) {
const { widget } = service;
- const { data: xteveData, error: xteveError } = useWidgetAPI(widget, "api");
+ const { data: xteveData, error: xteveError } = useWidgetAPI(widget);
if (xteveError) {
return ;
diff --git a/src/widgets/xteve/proxy.js b/src/widgets/xteve/proxy.js
index a8b1c80f2..421f2b499 100644
--- a/src/widgets/xteve/proxy.js
+++ b/src/widgets/xteve/proxy.js
@@ -7,7 +7,7 @@ import getServiceWidget from "utils/config/service-helpers";
const logger = createLogger("xteveProxyHandler");
export default async function xteveProxyHandler(req, res) {
- const { group, service, endpoint } = req.query;
+ const { group, service } = req.query;
if (!group || !service) {
return res.status(400).json({ error: "Invalid proxy service type" });
@@ -19,7 +19,7 @@ export default async function xteveProxyHandler(req, res) {
return res.status(403).json({ error: "Service does not support API calls" });
}
- const url = formatApiCall(api, { endpoint, ...widget });
+ const url = formatApiCall(api, { endpoint: "api/", ...widget });
const method = "POST";
const payload = { cmd: "status" };
diff --git a/src/widgets/xteve/widget.js b/src/widgets/xteve/widget.js
index e7998e2e7..72c62b253 100644
--- a/src/widgets/xteve/widget.js
+++ b/src/widgets/xteve/widget.js
@@ -3,12 +3,6 @@ import xteveProxyHandler from "./proxy";
const widget = {
api: "{url}/{endpoint}",
proxyHandler: xteveProxyHandler,
-
- mappings: {
- api: {
- endpoint: "api/",
- },
- },
};
export default widget;