diff --git a/package.json b/package.json index da430581c..95ee8f71e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dockerode": "^3.3.3", "js-yaml": "^4.1.0", "json-rpc-2.0": "^1.3.0", + "lodash": "^4.17.21", "memory-cache": "^0.2.0", "next": "12.2.5", "node-os-utils": "^1.3.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b4915e98..8a2bfa261 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ specifiers: eslint-config-next: 12.2.5 js-yaml: ^4.1.0 json-rpc-2.0: ^1.3.0 + lodash: ^4.17.21 memory-cache: ^0.2.0 next: 12.2.5 node-os-utils: ^1.3.7 @@ -28,6 +29,7 @@ dependencies: dockerode: 3.3.3 js-yaml: 4.1.0 json-rpc-2.0: 1.3.0 + lodash: 4.17.21 memory-cache: 0.2.0 next: 12.2.5_biqbaboplfbrettd7655fr4n2y node-os-utils: 1.3.7 @@ -43,7 +45,7 @@ devDependencies: eslint: 8.22.0 eslint-config-next: 12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq postcss: 8.4.16 - tailwindcss: 3.1.8 + tailwindcss: 3.1.8_postcss@8.4.16 typescript: 4.7.4 packages: @@ -271,7 +273,7 @@ packages: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.1.8 + tailwindcss: 3.1.8_postcss@8.4.16 dev: false /@types/json5/0.0.29: @@ -1558,6 +1560,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /loose-envify/1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2245,10 +2251,12 @@ packages: react: 18.2.0 dev: false - /tailwindcss/3.1.8: + /tailwindcss/3.1.8_postcss@8.4.16: resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==} engines: {node: '>=12.13.0'} hasBin: true + peerDependencies: + postcss: ^8.0.9 dependencies: arg: 5.0.2 chokidar: 3.5.3 diff --git a/src/components/services/widgets/service/docker.jsx b/src/components/services/widgets/service/docker.jsx index 4a41565d0..7a6729cc4 100644 --- a/src/components/services/widgets/service/docker.jsx +++ b/src/components/services/widgets/service/docker.jsx @@ -1,24 +1,24 @@ import useSWR from "swr"; -import { calculateCPUPercent, formatBytes } from "utils/stats-helpers"; - import Widget from "../widget"; import Block from "../block"; +import { calculateCPUPercent, formatBytes } from "utils/stats-helpers"; + export default function Docker({ service }) { const config = service.widget; const { data: statusData, error: statusError } = useSWR( `/api/docker/status/${config.container}/${config.server || ""}`, { - refreshInterval: 1500, + refreshInterval: 5000, } ); const { data: statsData, error: statsError } = useSWR( `/api/docker/stats/${config.container}/${config.server || ""}`, { - refreshInterval: 1500, + refreshInterval: 5000, } ); diff --git a/src/components/services/widgets/service/emby.jsx b/src/components/services/widgets/service/emby.jsx index af22338c2..ac9645665 100644 --- a/src/components/services/widgets/service/emby.jsx +++ b/src/components/services/widgets/service/emby.jsx @@ -3,17 +3,12 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Emby({ service, title = "Emby" }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url, key } = config; - return `${url}/emby/${endpoint}?api_key=${key}`; - } - - const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), { - refreshInterval: 1000, - }); + const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions")); if (sessionsError) { return ; diff --git a/src/components/services/widgets/service/jellyseerr.jsx b/src/components/services/widgets/service/jellyseerr.jsx index 9d1ebad8c..3820c2a16 100644 --- a/src/components/services/widgets/service/jellyseerr.jsx +++ b/src/components/services/widgets/service/jellyseerr.jsx @@ -3,49 +3,32 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; -export default function Jellyseerr({ service }) { - const config = service.widget; - - function buildApiUrl(endpoint) { - const { url } = config; - const reqUrl = new URL(`/api/v1/${endpoint}`, url); - return `/api/proxy?url=${encodeURIComponent(reqUrl)}`; - } +import { formatApiUrl } from "utils/api-helpers"; - const fetcher = async (url) => { - const res = await fetch(url, { - method: "GET", - withCredentials: true, - credentials: "include", - headers: { - "X-Api-Key": `${config.key}`, - "Content-Type": "application/json" - } - }); - return await res.json(); - }; - - const { data: statsData, error: statsError } = useSWR(buildApiUrl(`request/count`), fetcher); +export default function Jellyseerr({ service }) { + const config = service.widget; - if (statsError) { - return ; - } + const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`)); - if (!statsData) { - return ( - - - - - - ); - } + if (statsError) { + return ; + } + if (!statsData) { return ( - - - - - + + + + + ); + } + + return ( + + + + + + ); } diff --git a/src/components/services/widgets/service/npm.jsx b/src/components/services/widgets/service/npm.jsx index 083ee27a8..916136e10 100644 --- a/src/components/services/widgets/service/npm.jsx +++ b/src/components/services/widgets/service/npm.jsx @@ -3,39 +3,12 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Npm({ service }) { const config = service.widget; - const { url } = config; - - const fetcher = async (reqUrl) => { - const { url, username, password } = config; - const loginUrl = `${url}/api/tokens`; - const body = { identity: username, secret: password }; - - const res = await fetch(loginUrl, { - method: "POST", - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => response.json()) - .then( - async (data) => - await fetch(reqUrl, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + data.token, - }, - }) - ); - return res.json(); - }; - - const { data: infoData, error: infoError } = useSWR(`${url}/api/nginx/proxy-hosts`, fetcher); - console.log(infoData); + const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts")); if (infoError) { return ; diff --git a/src/components/services/widgets/service/nzbget.jsx b/src/components/services/widgets/service/nzbget.jsx index 87d77b91d..56baa6312 100644 --- a/src/components/services/widgets/service/nzbget.jsx +++ b/src/components/services/widgets/service/nzbget.jsx @@ -1,44 +1,15 @@ import useSWR from "swr"; -import { JSONRPCClient } from "json-rpc-2.0"; - -import { formatBytes } from "utils/stats-helpers"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; +import { formatBytes } from "utils/stats-helpers"; + export default function Nzbget({ service }) { const config = service.widget; - const constructedUrl = new URL(config.url); - constructedUrl.pathname = "jsonrpc"; - - const client = new JSONRPCClient((jsonRPCRequest) => - fetch(constructedUrl.toString(), { - method: "POST", - headers: { - "content-type": "application/json", - authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`, - }, - body: JSON.stringify(jsonRPCRequest), - }).then(async (response) => { - if (response.status === 200) { - const jsonRPCResponse = await response.json(); - return client.receive(jsonRPCResponse); - } else if (jsonRPCRequest.id !== undefined) { - return Promise.reject(new Error(response.statusText)); - } - }) - ); - - const { data: statusData, error: statusError } = useSWR( - "status", - (resource) => { - return client.request(resource).then((response) => response); - }, - { - refreshInterval: 1000, - } - ); + const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status")); if (statusError) { return ; diff --git a/src/components/services/widgets/service/ombi.jsx b/src/components/services/widgets/service/ombi.jsx index 54c44f7a3..4bbf96fdb 100644 --- a/src/components/services/widgets/service/ombi.jsx +++ b/src/components/services/widgets/service/ombi.jsx @@ -3,27 +3,12 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Ombi({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url } = config; - return `${url}/api/v1/${endpoint}`; - } - - const fetcher = (url) => { - return fetch(url, { - method: "GET", - withCredentials: true, - credentials: "include", - headers: { - ApiKey: `${config.key}`, - "Content-Type": "application/json", - }, - }).then((res) => res.json()); - }; - - const { data: statsData, error: statsError } = useSWR(buildApiUrl(`Request/count`), fetcher); + const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`)); if (statsError) { return ; diff --git a/src/components/services/widgets/service/pihole.jsx b/src/components/services/widgets/service/pihole.jsx index c29b5d6c1..7c777ce76 100644 --- a/src/components/services/widgets/service/pihole.jsx +++ b/src/components/services/widgets/service/pihole.jsx @@ -3,21 +3,12 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Pihole({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url, proxy } = config; - - if (proxy) { - const fullUrl = `${url}/admin/${endpoint}`; - return "/api/proxy?url=" + encodeURIComponent(fullUrl); - } - - return `${url}/admin/${endpoint}`; - } - - const { data: piholeData, error: piholeError } = useSWR(buildApiUrl("api.php")); + const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php")); if (piholeError) { return ; diff --git a/src/components/services/widgets/service/portainer.jsx b/src/components/services/widgets/service/portainer.jsx index ed3d184a6..1e97052d8 100644 --- a/src/components/services/widgets/service/portainer.jsx +++ b/src/components/services/widgets/service/portainer.jsx @@ -3,29 +3,12 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Portainer({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url, env } = config; - const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url); - return `/api/proxy?url=${encodeURIComponent(reqUrl)}`; - } - - const fetcher = async (url) => { - const res = await fetch(url, { - method: "GET", - withCredentials: true, - credentials: "include", - headers: { - "X-API-Key": `${config.key}`, - "Content-Type": "application/json", - }, - }); - return await res.json(); - }; - - const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json?all=1`), fetcher); + const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`)); if (containersError) { return ; diff --git a/src/components/services/widgets/service/radarr.jsx b/src/components/services/widgets/service/radarr.jsx index 4e4bb3c60..fec155c82 100644 --- a/src/components/services/widgets/service/radarr.jsx +++ b/src/components/services/widgets/service/radarr.jsx @@ -3,16 +3,13 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Radarr({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url, key } = config; - return `${url}/api/v3/${endpoint}?apikey=${key}`; - } - - const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie")); - const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue/status")); + const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie")); + const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status")); if (moviesError || queuedError) { return ; diff --git a/src/components/services/widgets/service/rutorrent.jsx b/src/components/services/widgets/service/rutorrent.jsx index e5cb12b68..5dd113336 100644 --- a/src/components/services/widgets/service/rutorrent.jsx +++ b/src/components/services/widgets/service/rutorrent.jsx @@ -1,32 +1,15 @@ import useSWR from "swr"; -import RuTorrent from "rutorrent-promise"; - -import { formatBytes } from "utils/stats-helpers"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; +import { formatBytes } from "utils/stats-helpers"; + export default function Rutorrent({ service }) { const config = service.widget; - function buildApiUrl() { - const { url, username, password } = config; - - const options = { - url: `${url}/plugins/httprpc/action.php`, - }; - - if (username && password) { - options.username = username; - options.password = password; - } - - const params = new URLSearchParams(options); - - return `/api/widgets/rutorrent?${params.toString()}`; - } - - const { data: statusData, error: statusError } = useSWR(buildApiUrl()); + const { data: statusData, error: statusError } = useSWR(formatApiUrl(config)); if (statusError) { return ; diff --git a/src/components/services/widgets/service/sonarr.jsx b/src/components/services/widgets/service/sonarr.jsx index 36afee20e..fe042a19d 100644 --- a/src/components/services/widgets/service/sonarr.jsx +++ b/src/components/services/widgets/service/sonarr.jsx @@ -3,17 +3,14 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Sonarr({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url, key } = config; - return `${url}/api/v3/${endpoint}?apikey=${key}`; - } - - const { data: wantedData, error: wantedError } = useSWR(buildApiUrl("wanted/missing")); - const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue")); - const { data: seriesData, error: seriesError } = useSWR(buildApiUrl("series")); + const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); + const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue")); + const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series")); if (wantedError || queuedError || seriesError) { return ; diff --git a/src/components/services/widgets/service/speedtest.jsx b/src/components/services/widgets/service/speedtest.jsx index b2e28007a..7bcf884d8 100644 --- a/src/components/services/widgets/service/speedtest.jsx +++ b/src/components/services/widgets/service/speedtest.jsx @@ -4,16 +4,12 @@ import Widget from "../widget"; import Block from "../block"; import { formatBits } from "utils/stats-helpers"; +import { formatApiUrl } from "utils/api-helpers"; export default function Speedtest({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url } = config; - return `${url}/api/${endpoint}`; - } - - const { data: speedtestData, error: speedtestError } = useSWR(buildApiUrl("speedtest/latest")); + const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest")); if (speedtestError) { return ; @@ -31,8 +27,8 @@ export default function Speedtest({ service }) { return ( - - + + ); diff --git a/src/components/services/widgets/service/tautulli.jsx b/src/components/services/widgets/service/tautulli.jsx index bed8afa29..32fe91b37 100644 --- a/src/components/services/widgets/service/tautulli.jsx +++ b/src/components/services/widgets/service/tautulli.jsx @@ -3,18 +3,12 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Tautulli({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url, key } = config; - const fullUrl = `${url}/api/v2?apikey=${key}&cmd=${endpoint}`; - return "/api/proxy?url=" + encodeURIComponent(fullUrl); - } - - const { data: statsData, error: statsError } = useSWR(buildApiUrl("get_activity"), { - refreshInterval: 1000, - }); + const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity")); if (statsError) { return ; diff --git a/src/components/services/widgets/service/traefik.jsx b/src/components/services/widgets/service/traefik.jsx index 05811ebb4..ce3fd4fe3 100644 --- a/src/components/services/widgets/service/traefik.jsx +++ b/src/components/services/widgets/service/traefik.jsx @@ -3,16 +3,12 @@ import useSWR from "swr"; import Widget from "../widget"; import Block from "../block"; +import { formatApiUrl } from "utils/api-helpers"; + export default function Traefik({ service }) { const config = service.widget; - function buildApiUrl(endpoint) { - const { url } = config; - const fullUrl = `${url}/api/${endpoint}`; - return `/api/proxy?url=${encodeURIComponent(fullUrl)}`; - } - - const { data: traefikData, error: traefikError } = useSWR(buildApiUrl("overview")); + const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview")); if (traefikError) { return ; diff --git a/src/pages/api/services/index.js b/src/pages/api/services/index.js index 53d508692..572ab4048 100644 --- a/src/pages/api/services/index.js +++ b/src/pages/api/services/index.js @@ -15,10 +15,23 @@ export default async function handler(req, res) { return { name: Object.keys(group)[0], services: group[Object.keys(group)[0]].map((entries) => { - return { + const { widget, ...service } = entries[Object.keys(entries)[0]]; + let res = { name: Object.keys(entries)[0], - ...entries[Object.keys(entries)[0]], + ...service, }; + + if (widget) { + const { type } = widget; + + res.widget = { + type: type, + service_group: Object.keys(group)[0], + service_name: Object.keys(entries)[0], + }; + } + + return res; }), }; }); diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js new file mode 100644 index 000000000..029b81878 --- /dev/null +++ b/src/pages/api/services/proxy.js @@ -0,0 +1,36 @@ +import genericProxyHandler from "utils/proxies/generic"; +import credentialedProxyHandler from "utils/proxies/credentialed"; +import rutorrentProxyHandler from "utils/proxies/rutorrent"; +import nzbgetProxyHandler from "utils/proxies/nzbget"; +import npmProxyHandler from "utils/proxies/npm"; + +const serviceProxyHandlers = { + // uses query param auth + emby: genericProxyHandler, + pihole: genericProxyHandler, + radarr: genericProxyHandler, + sonarr: genericProxyHandler, + speedtest: genericProxyHandler, + tautulli: genericProxyHandler, + traefik: genericProxyHandler, + // uses X-API-Key header auth + portainer: credentialedProxyHandler, + jellyseerr: credentialedProxyHandler, + ombi: credentialedProxyHandler, + // super specific handlers + rutorrent: rutorrentProxyHandler, + nzbget: nzbgetProxyHandler, + npm: npmProxyHandler, +}; + +export default async function handler(req, res) { + const { type } = req.query; + + const serviceProxyHandler = serviceProxyHandlers[type]; + + if (serviceProxyHandler) { + return serviceProxyHandler(req, res); + } + + res.status(403).json({ error: "Unkown proxy service type" }); +} diff --git a/src/pages/api/widgets/rutorrent.js b/src/pages/api/widgets/rutorrent.js deleted file mode 100644 index 1e60edd90..000000000 --- a/src/pages/api/widgets/rutorrent.js +++ /dev/null @@ -1,23 +0,0 @@ -import RuTorrent from "rutorrent-promise"; - -// TODO: Remove the 3rd party dependency once I figure out how to -// call this myself with fetch. Just need to destruct the package. - -export default async function handler(req, res) { - const { url, username, password } = req.query; - - const constructedUrl = new URL(url); - - const rutorrent = new RuTorrent({ - host: constructedUrl.hostname, // default: localhost - port: constructedUrl.port, // default: 80 - path: constructedUrl.pathname, // default: /rutorrent - ssl: constructedUrl.protocol === "https:", // default: false - username: username, // default: none - password: password, // default: none - }); - - const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]); - - res.status(200).send(data); -} diff --git a/src/utils/api-helpers.js b/src/utils/api-helpers.js new file mode 100644 index 000000000..fa7e489b6 --- /dev/null +++ b/src/utils/api-helpers.js @@ -0,0 +1,34 @@ +const formats = { + emby: `{url}/emby/{endpoint}?api_key={key}`, + pihole: `{url}/admin/{endpoint}`, + radarr: `{url}/api/v3/{endpoint}?apikey={key}`, + sonarr: `{url}/api/v3/{endpoint}?apikey={key}`, + speedtest: `{url}/api/{endpoint}`, + tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`, + traefik: `{url}/api/{endpoint}`, + portainer: `{url}/api/endpoints/{env}/{endpoint}`, + rutorrent: `{url}/plugins/httprpc/action.php`, + jellyseerr: `{url}/api/v1/{endpoint}`, + ombi: `{url}/api/v1/{endpoint}`, + npm: `{url}/api/{endpoint}`, +}; + +export function formatApiCall(api, args) { + const match = /\{.*?\}/g; + const replace = (match) => { + const key = match.replace(/\{|\}/g, ""); + return args[key]; + }; + + return formats[api].replace(match, replace); +} + +export function formatApiUrl(widget, endpoint) { + const params = new URLSearchParams({ + type: widget.type, + group: widget.service_group, + service: widget.service_name, + endpoint, + }); + return `/api/services/proxy?${params.toString()}`; +} diff --git a/src/utils/http.js b/src/utils/http.js index 8d37a2b3d..91c8c5fb9 100644 --- a/src/utils/http.js +++ b/src/utils/http.js @@ -44,3 +44,20 @@ export function httpRequest(url, params) { request.end(); }); } + +export function httpProxy(url, params = {}) { + const constructedUrl = new URL(url); + + if (constructedUrl.protocol === "https:") { + const httpsAgent = new https.Agent({ + rejectUnauthorized: false, + }); + + return httpsRequest(constructedUrl, { + agent: httpsAgent, + ...params, + }); + } else { + return httpRequest(constructedUrl, params); + } +} diff --git a/src/utils/proxies/credentialed.js b/src/utils/proxies/credentialed.js new file mode 100644 index 000000000..f13487465 --- /dev/null +++ b/src/utils/proxies/credentialed.js @@ -0,0 +1,28 @@ +import { getServiceWidget } from "utils/service-helpers"; +import { formatApiCall } from "utils/api-helpers"; +import { httpProxy } from "utils/http"; + +export default async function credentialedProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); + const [status, contentType, data] = await httpProxy(url, { + withCredentials: true, + credentials: "include", + headers: { + "X-API-Key": `${widget.key}`, + "Content-Type": "application/json", + }, + }); + + res.setHeader("Content-Type", contentType); + return res.status(status).send(data); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/utils/proxies/generic.js b/src/utils/proxies/generic.js new file mode 100644 index 000000000..6899ebf91 --- /dev/null +++ b/src/utils/proxies/generic.js @@ -0,0 +1,21 @@ +import { getServiceWidget } from "utils/service-helpers"; +import { formatApiCall } from "utils/api-helpers"; +import { httpProxy } from "utils/http"; + +export default async function genericProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); + const [status, contentType, data] = await httpProxy(url); + + res.setHeader("Content-Type", contentType); + return res.status(status).send(data); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/utils/proxies/npm.js b/src/utils/proxies/npm.js new file mode 100644 index 000000000..d6c68cd86 --- /dev/null +++ b/src/utils/proxies/npm.js @@ -0,0 +1,37 @@ +import { getServiceWidget } from "utils/service-helpers"; +import { formatApiCall } from "utils/api-helpers"; + +export default async function npmProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); + + const loginUrl = `${widget.url}/api/tokens`; + const body = { identity: widget.username, secret: widget.password }; + + const authResponse = await fetch(loginUrl, { + method: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + }, + }).then((response) => response.json()); + + const apiResponse = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + authResponse.token, + }, + }).then((response) => response.json()); + + return res.send(apiResponse); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/utils/proxies/nzbget.js b/src/utils/proxies/nzbget.js new file mode 100644 index 000000000..7bf078b44 --- /dev/null +++ b/src/utils/proxies/nzbget.js @@ -0,0 +1,39 @@ +import { JSONRPCClient } from "json-rpc-2.0"; +import { getServiceWidget } from "utils/service-helpers"; + +export default async function nzbgetProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + const constructedUrl = new URL(widget.url); + constructedUrl.pathname = "jsonrpc"; + + const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64"); + + const client = new JSONRPCClient((jsonRPCRequest) => + fetch(constructedUrl.toString(), { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Basic ${authorization}`, + }, + body: JSON.stringify(jsonRPCRequest), + }).then(async (response) => { + if (response.status === 200) { + const jsonRPCResponse = await response.json(); + return client.receive(jsonRPCResponse); + } else if (jsonRPCRequest.id !== undefined) { + return Promise.reject(new Error(response.statusText)); + } + }) + ); + + return res.send(await client.request(endpoint)); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/utils/proxies/rutorrent.js b/src/utils/proxies/rutorrent.js new file mode 100644 index 000000000..3f9dda3ce --- /dev/null +++ b/src/utils/proxies/rutorrent.js @@ -0,0 +1,30 @@ +import RuTorrent from "rutorrent-promise"; + +import { getServiceWidget } from "utils/service-helpers"; + +export default async function rutorrentProxyHandler(req, res) { + const { group, service } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + const constructedUrl = new URL(widget.url); + + const rutorrent = new RuTorrent({ + host: constructedUrl.hostname, + port: constructedUrl.port, + path: constructedUrl.pathname, + ssl: constructedUrl.protocol === "https:", + username: widget.username, + password: widget.password, + }); + + const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]); + + return res.status(200).send(data); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/utils/service-helpers.js b/src/utils/service-helpers.js new file mode 100644 index 000000000..986fc2272 --- /dev/null +++ b/src/utils/service-helpers.js @@ -0,0 +1,33 @@ +import { promises as fs } from "fs"; +import path from "path"; +import yaml from "js-yaml"; + +export async function getServiceWidget(group, service) { + const servicesYaml = path.join(process.cwd(), "config", "services.yaml"); + const fileContents = await fs.readFile(servicesYaml, "utf8"); + const services = yaml.load(fileContents); + + // map easy to write YAML objects into easy to consume JS arrays + const servicesArray = services.map((group) => { + return { + name: Object.keys(group)[0], + services: group[Object.keys(group)[0]].map((entries) => { + return { + name: Object.keys(entries)[0], + ...entries[Object.keys(entries)[0]], + }; + }), + }; + }); + + const serviceGroup = servicesArray.find((g) => g.name === group); + if (serviceGroup) { + const serviceEntry = serviceGroup.services.find((s) => s.name === service); + if (serviceEntry) { + const { widget } = serviceEntry; + return widget; + } + } + + return false; +}