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