|
@@ -0,0 +1,191 @@
|
|
|
|
+/* eslint-disable no-underscore-dangle */
|
|
|
|
+import crypto from 'crypto';
|
|
|
|
+import querystring from 'querystring';
|
|
|
|
+
|
|
|
|
+import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools"
|
|
|
|
+
|
|
|
|
+import getServiceWidget from "utils/config/service-helpers";
|
|
|
|
+import { httpProxy } from "utils/proxy/http";
|
|
|
|
+import createLogger from "utils/logger";
|
|
|
|
+
|
|
|
|
+const proxyName = "jdownloaderProxyHandler";
|
|
|
|
+const logger = createLogger(proxyName);
|
|
|
|
+
|
|
|
|
+async function getWidget(req) {
|
|
|
|
+ const { group, service } = req.query;
|
|
|
|
+ if (!group || !service) {
|
|
|
|
+ logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ const widget = await getServiceWidget(group, service);
|
|
|
|
+ if (!widget) {
|
|
|
|
+ logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return widget;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+async function login(loginSecret, deviceSecret, params) {
|
|
|
|
+ const rid = uniqueRid();
|
|
|
|
+ const path = `/my/connect?${querystring.stringify({...params, rid})}`;
|
|
|
|
+
|
|
|
|
+ const signature = crypto
|
|
|
|
+ .createHmac('sha256', loginSecret)
|
|
|
|
+ .update(path)
|
|
|
|
+ .digest('hex');
|
|
|
|
+ const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
|
|
|
|
+
|
|
|
|
+ const [status, contentType, data] = await httpProxy(url, {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ headers: {
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
+ },
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ if (status !== 200) {
|
|
|
|
+ logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
|
|
|
+ return [status, data];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ const decryptedData = JSON.parse(decrypt(data.toString(), loginSecret))
|
|
|
|
+ const sessionToken = decryptedData.sessiontoken;
|
|
|
|
+ validateRid(decryptedData, rid);
|
|
|
|
+ const serverEncryptionToken = createEncryptionToken(loginSecret, sessionToken);
|
|
|
|
+ const deviceEncryptionToken = createEncryptionToken(deviceSecret, sessionToken);
|
|
|
|
+ return [status, decryptedData, contentType, serverEncryptionToken, deviceEncryptionToken, sessionToken];
|
|
|
|
+ } catch (e) {
|
|
|
|
+ logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
|
|
|
+ return [status, null];
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+async function getDevice(serverEncryptionToken, deviceName, params) {
|
|
|
|
+ const rid = uniqueRid();
|
|
|
|
+ const path = `/my/listdevices?${querystring.stringify({...params, rid})}`;
|
|
|
|
+ const signature = crypto
|
|
|
|
+ .createHmac('sha256', serverEncryptionToken)
|
|
|
|
+ .update(path)
|
|
|
|
+ .digest('hex');
|
|
|
|
+ const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
|
|
|
|
+
|
|
|
|
+ const [status, , data] = await httpProxy(url, {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ headers: {
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
+ },
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ if (status !== 200) {
|
|
|
|
+ logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
|
|
|
+ return [status, data];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ const decryptedData = JSON.parse(decrypt(data.toString(), serverEncryptionToken))
|
|
|
|
+ const filteredDevice = decryptedData.list.filter(device => device.name === deviceName);
|
|
|
|
+ return [status, filteredDevice[0].id];
|
|
|
|
+ } catch (e) {
|
|
|
|
+ logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
|
|
|
+ return [status, null];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function createBody(rid, query, params) {
|
|
|
|
+ const baseBody = {
|
|
|
|
+ apiVer: 1,
|
|
|
|
+ rid,
|
|
|
|
+ url: query
|
|
|
|
+ };
|
|
|
|
+ return params ? {...baseBody, params: [JSON.stringify(params)] } : baseBody;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+async function queryPackages(deviceEncryptionToken, deviceId, sessionToken, params) {
|
|
|
|
+ const rid = uniqueRid();
|
|
|
|
+ const body = encrypt(JSON.stringify(createBody(rid, '/downloadsV2/queryPackages', params)), deviceEncryptionToken);
|
|
|
|
+ const url = `${new URL(`https://api.jdownloader.org/t_${encodeURI(sessionToken)}_${encodeURI(deviceId)}/downloadsV2/queryPackages`)}`
|
|
|
|
+ const [status, , data] = await httpProxy(url, {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ body,
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ if (status !== 200) {
|
|
|
|
+ logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
|
|
|
+ return [status, data];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ const decryptedData = JSON.parse(decrypt(data.toString(), deviceEncryptionToken))
|
|
|
|
+ return decryptedData.data;
|
|
|
|
+ } catch (e) {
|
|
|
|
+ logger.error("Error decoding JDRss jdownloader data. Data: %s", data.toString());
|
|
|
|
+ return [status, null];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+export default async function jdownloaderProxyHandler(req, res) {
|
|
|
|
+ const widget = await getWidget(req);
|
|
|
|
+
|
|
|
|
+ if (!widget) {
|
|
|
|
+ return res.status(400).json({ error: "Invalid proxy service type" });
|
|
|
|
+ }
|
|
|
|
+ logger.debug("Getting data from JDRss API");
|
|
|
|
+ const {username} = widget
|
|
|
|
+ const {password} = widget
|
|
|
|
+
|
|
|
|
+ const appKey = "homepage"
|
|
|
|
+ const loginSecret = sha256(`${username}${password}server`)
|
|
|
|
+ const deviceSecret = sha256(`${username}${password}device`)
|
|
|
|
+ const email = username;
|
|
|
|
+
|
|
|
|
+ const loginData = await login(loginSecret, deviceSecret, {
|
|
|
|
+ appKey,
|
|
|
|
+ email
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ const deviceData = await getDevice(loginData[3], widget.client, {
|
|
|
|
+ sessiontoken: loginData[5]
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ const packageStatus = await queryPackages(loginData[4], deviceData[1], loginData[5], {
|
|
|
|
+ "bytesLoaded": false,
|
|
|
|
+ "bytesTotal": true,
|
|
|
|
+ "comment": false,
|
|
|
|
+ "enabled": true,
|
|
|
|
+ "eta": false,
|
|
|
|
+ "priority": false,
|
|
|
|
+ "finished": true,
|
|
|
|
+ "running": true,
|
|
|
|
+ "speed": true,
|
|
|
|
+ "status": true,
|
|
|
|
+ "childCount": false,
|
|
|
|
+ "hosts": false,
|
|
|
|
+ "saveTo": false,
|
|
|
|
+ "maxResults": -1,
|
|
|
|
+ "startAt": 0,
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ let totalBytes = 0;
|
|
|
|
+ let totalSpeed = 0;
|
|
|
|
+ packageStatus.forEach(file => {
|
|
|
|
+ totalBytes += file.bytesTotal;
|
|
|
|
+ if (file.speed) {
|
|
|
|
+ totalSpeed += file.speed;
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const data = {
|
|
|
|
+ downloadCount: packageStatus.length,
|
|
|
|
+ totalBytes,
|
|
|
|
+ totalSpeed
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ return res.send(data);
|
|
|
|
+
|
|
|
|
+}
|