diff --git a/package.json b/package.json index a08072df..5f486bf7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 8080", "build": "next build", "start": "next start", "lint": "next lint", @@ -17,7 +17,6 @@ "dockerode": "^3.3.4", "follow-redirects": "^1.15.2", "i18next": "^21.9.2", - "jdownloader-client": "^1.0.0", "js-yaml": "^4.1.0", "json-rpc-2.0": "^1.4.1", "memory-cache": "^0.2.0", diff --git a/src/widgets/jdownloader/proxy.js b/src/widgets/jdownloader/proxy.js index 6410633c..44eadcad 100644 --- a/src/widgets/jdownloader/proxy.js +++ b/src/widgets/jdownloader/proxy.js @@ -1,8 +1,12 @@ /* eslint-disable no-underscore-dangle */ -import getServiceWidget from "utils/config/service-helpers"; -import createLogger from "utils/logger"; +import crypto from 'crypto'; +import querystring from 'querystring'; -const { JDownloaderClient } = require('jdownloader-client') +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); @@ -22,6 +26,108 @@ async function getWidget(req) { 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); @@ -29,10 +135,24 @@ export default async function jdownloaderProxyHandler(req, res) { return res.status(400).json({ error: "Invalid proxy service type" }); } logger.debug("Getting data from JDRss API"); - const client = new JDownloaderClient(widget.username, widget.password) - await client.connect() - const devices = await client.listDevices() - const packageStatus = await client.downloadsQueryPackages(devices[0].id, { + 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, @@ -48,7 +168,9 @@ export default async function jdownloaderProxyHandler(req, res) { "saveTo": false, "maxResults": -1, "startAt": 0, - }) + } + ) + let totalBytes = 0; let totalSpeed = 0; packageStatus.forEach(file => { diff --git a/src/widgets/jdownloader/tools.js b/src/widgets/jdownloader/tools.js new file mode 100644 index 00000000..d678b072 --- /dev/null +++ b/src/widgets/jdownloader/tools.js @@ -0,0 +1,55 @@ +import crypto from 'crypto'; + +export function sha256(data) { + return crypto + .createHash('sha256') + .update(data) + .digest(); +} + +export function uniqueRid() { + return Math.floor(Math.random() * 10e12); +} + +export function validateRid(decryptedData, rid) { + if (decryptedData.rid !== rid) { + throw new Error('RequestID mismatch'); + } + return decryptedData; + +} + +export function decrypt(data, ivKey) { + const iv = ivKey.slice(0, ivKey.length / 2); + const key = ivKey.slice(ivKey.length / 2, ivKey.length); + const cipher = crypto.createDecipheriv('aes-128-cbc', key, iv); + return Buffer.concat([ + cipher.update(data, 'base64'), + cipher.final() + ]).toString(); +} + +export function createEncryptionToken(oldTokenBuff, updateToken) { + const updateTokenBuff = Buffer.from(updateToken, 'hex'); + const mergedBuffer = Buffer.concat([oldTokenBuff, updateTokenBuff], oldTokenBuff.length + updateTokenBuff.length); + return sha256(mergedBuffer); +} + +export function encrypt(data, ivKey) { + if (typeof data !== 'string') { + throw new Error('data no es un string'); + } + if (!(ivKey instanceof Buffer)) { + throw new Error('ivKey no es un buffer'); + } + if (ivKey.length !== 32) { + throw new Error('ivKey tiene que tener tamaƱo 32'); + } + const stringIVKey = ivKey.toString('hex'); + const stringIV = stringIVKey.substring(0, stringIVKey.length / 2); + const stringKey = stringIVKey.substring(stringIVKey.length / 2, stringIVKey.length); + const iv = Buffer.from(stringIV, 'hex'); + const key = Buffer.from(stringKey, 'hex'); + const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); + return cipher.update(data, 'utf8', 'base64') + cipher.final('base64'); +} \ No newline at end of file diff --git a/src/widgets/jdownloader/widget.js b/src/widgets/jdownloader/widget.js index 2685b571..d3213740 100644 --- a/src/widgets/jdownloader/widget.js +++ b/src/widgets/jdownloader/widget.js @@ -1,12 +1,13 @@ import jdownloaderProxyHandler from "./proxy"; const widget = { - api: "{url}/api/{endpoint}", + api: "https://api.jdownloader.org/{endpoint}/&signature={signature}", proxyHandler: jdownloaderProxyHandler, mappings: { unified: { endpoint: "/", + signature: "", }, }, };