this passes all widget API calls through the backend, with a pluggable design and reusable API handlerspull/62/head
parent
975f79f6cc
commit
97bf174b78
@ -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" });
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
@ -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()}`;
|
||||||
|
}
|
@ -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" });
|
||||||
|
}
|
@ -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" });
|
||||||
|
}
|
@ -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" });
|
||||||
|
}
|
@ -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" });
|
||||||
|
}
|
@ -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" });
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in new issue