feat: adding authorization to rpc

This commit is contained in:
Chubby Granny Chaser 2024-06-28 12:03:01 +01:00
parent 96e96cd8aa
commit 363bcf16a4
No known key found for this signature in database
6 changed files with 166 additions and 49 deletions

View File

@ -0,0 +1,121 @@
import path from "node:path";
import fs from "node:fs";
import crypto from "node:crypto";
import axios, { type AxiosProgressEvent } from "axios";
import { app } from "electron";
import { logger } from "../logger";
export class HttpDownload {
private abortController: AbortController;
public lastProgressEvent: AxiosProgressEvent;
private trackerFilePath: string;
private trackerProgressEvent: AxiosProgressEvent | null = null;
private downloadPath: string;
private downloadTrackersPath = path.join(
app.getPath("documents"),
"Hydra",
"Downloads"
);
constructor(
private url: string,
private savePath: string
) {
this.abortController = new AbortController();
const sha256Hasher = crypto.createHash("sha256");
const hash = sha256Hasher.update(url).digest("hex");
this.trackerFilePath = path.join(
this.downloadTrackersPath,
`${hash}.hydradownload`
);
const filename = path.win32.basename(this.url);
this.downloadPath = path.join(this.savePath, filename);
}
private updateTrackerFile() {
if (!fs.existsSync(this.downloadTrackersPath)) {
fs.mkdirSync(this.downloadTrackersPath, {
recursive: true,
});
}
fs.writeFileSync(
this.trackerFilePath,
JSON.stringify(this.lastProgressEvent),
{ encoding: "utf-8" }
);
}
private removeTrackerFile() {
if (fs.existsSync(this.trackerFilePath)) {
fs.rm(this.trackerFilePath, () => {});
}
}
public async startDownload() {
// Check if there's already a tracker file and download file
if (
fs.existsSync(this.trackerFilePath) &&
fs.existsSync(this.downloadPath)
) {
this.trackerProgressEvent = JSON.parse(
fs.readFileSync(this.trackerFilePath, { encoding: "utf-8" })
);
}
const response = await axios.get(this.url, {
responseType: "stream",
signal: this.abortController.signal,
headers: {
Range: `bytes=${this.trackerProgressEvent?.loaded ?? 0}-`,
},
onDownloadProgress: (progressEvent) => {
const total =
this.trackerProgressEvent?.total ?? progressEvent.total ?? 0;
const loaded =
(this.trackerProgressEvent?.loaded ?? 0) + progressEvent.loaded;
const progress = loaded / total;
this.lastProgressEvent = {
...progressEvent,
total,
progress,
loaded,
};
this.updateTrackerFile();
if (progressEvent.progress === 1) {
this.removeTrackerFile();
}
},
});
response.data.pipe(
fs.createWriteStream(this.downloadPath, {
flags: "a",
})
);
}
public async pauseDownload() {
this.abortController.abort();
}
public cancelDownload() {
this.pauseDownload();
fs.rm(this.downloadPath, (err) => {
if (err) logger.error(err);
});
fs.rm(this.trackerFilePath, (err) => {
if (err) logger.error(err);
});
}
}

View File

@ -1,18 +1,15 @@
import fs from "node:fs";
import path from "node:path";
import { Game } from "@main/entity";
import { RealDebridClient } from "../real-debrid";
import axios, { AxiosProgressEvent } from "axios";
import { gameRepository } from "@main/repository";
import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download";
export class RealDebridDownloader {
private static downloadingGame: Game | null = null;
private static realDebridTorrentId: string | null = null;
private static lastProgressEvent: AxiosProgressEvent | null = null;
private static abortController: AbortController | null = null;
private static httpDownload: HttpDownload | null = null;
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
@ -38,13 +35,15 @@ export class RealDebridDownloader {
}
public static async getStatus() {
if (this.lastProgressEvent) {
const lastProgressEvent = this.httpDownload?.lastProgressEvent;
if (lastProgressEvent) {
await gameRepository.update(
{ id: this.downloadingGame!.id },
{
bytesDownloaded: this.lastProgressEvent.loaded,
fileSize: this.lastProgressEvent.total,
progress: this.lastProgressEvent.progress,
bytesDownloaded: lastProgressEvent.loaded,
fileSize: lastProgressEvent.total,
progress: lastProgressEvent.progress,
status: "active",
}
);
@ -52,19 +51,19 @@ export class RealDebridDownloader {
const progress = {
numPeers: 0,
numSeeds: 0,
downloadSpeed: this.lastProgressEvent.rate,
downloadSpeed: lastProgressEvent.rate,
timeRemaining: calculateETA(
this.lastProgressEvent.total ?? 0,
this.lastProgressEvent.loaded,
this.lastProgressEvent.rate ?? 0
lastProgressEvent.total ?? 0,
lastProgressEvent.loaded,
lastProgressEvent.rate ?? 0
),
isDownloadingMetadata: false,
isCheckingFiles: false,
progress: this.lastProgressEvent.progress,
progress: lastProgressEvent.progress,
gameId: this.downloadingGame!.id,
} as DownloadProgress;
if (this.lastProgressEvent.progress === 1) {
if (lastProgressEvent.progress === 1) {
this.pauseDownload();
}
@ -102,13 +101,8 @@ export class RealDebridDownloader {
}
static async pauseDownload() {
if (this.abortController) {
this.abortController.abort();
}
this.abortController = null;
this.httpDownload?.pauseDownload();
this.realDebridTorrentId = null;
this.lastProgressEvent = null;
this.downloadingGame = null;
}
@ -120,30 +114,12 @@ export class RealDebridDownloader {
if (downloadUrl) {
this.realDebridTorrentId = null;
this.abortController = new AbortController();
const response = await axios.get(downloadUrl, {
responseType: "stream",
signal: this.abortController.signal,
onDownloadProgress: (progressEvent) => {
this.lastProgressEvent = progressEvent;
},
});
const filename = path.win32.basename(downloadUrl);
const downloadPath = path.join(game.downloadPath!, filename);
await gameRepository.update(
{ id: this.downloadingGame.id },
{ folderName: filename }
);
response.data.pipe(fs.createWriteStream(downloadPath));
this.httpDownload = new HttpDownload(downloadUrl, game!.downloadPath!);
this.httpDownload.startDownload();
}
}
static async cancelDownload() {
return this.pauseDownload();
static cancelDownload() {
return this.httpDownload?.cancelDownload();
}
}

View File

@ -1,5 +1,6 @@
import path from "node:path";
import cp from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import { app, dialog } from "electron";
import type { StartDownloadPayload } from "./types";
@ -12,11 +13,13 @@ const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
export const BITTORRENT_PORT = "5881";
export const RPC_PORT = "8084";
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
export const startTorrentClient = (args: StartDownloadPayload) => {
const commonArgs = [
BITTORRENT_PORT,
RPC_PORT,
RPC_PASSWORD,
encodeURIComponent(JSON.stringify(args)),
];

View File

@ -1,7 +1,7 @@
import cp from "node:child_process";
import { Game } from "@main/entity";
import { RPC_PORT, startTorrentClient } from "./torrent-client";
import { RPC_PASSWORD, RPC_PORT, startTorrentClient } from "./torrent-client";
import { gameRepository } from "@main/repository";
import { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
@ -21,6 +21,9 @@ export class TorrentDownloader {
private static rpc = axios.create({
baseURL: `http://localhost:${RPC_PORT}`,
headers: {
"x-hydra-rpc-password": RPC_PASSWORD,
},
});
private static spawn(args: StartDownloadPayload) {

View File

@ -22,13 +22,14 @@ export function useDownload() {
);
const dispatch = useAppDispatch();
const startDownload = (payload: StartGameDownloadPayload) =>
const startDownload = (payload: StartGameDownloadPayload) => {
dispatch(clearDownload());
window.electron.startGameDownload(payload).then((game) => {
dispatch(clearDownload());
updateLibrary();
return game;
});
};
const pauseDownload = async (gameId: number) => {
await window.electron.pauseGameDownload(gameId);
@ -65,7 +66,7 @@ export function useDownload() {
updateLibrary();
});
const getETA = () => {
const calculateETA = () => {
if (!lastPacket || lastPacket.timeRemaining < 0) return "";
try {
@ -87,7 +88,7 @@ export function useDownload() {
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
progress: formatDownloadProgress(lastPacket?.progress ?? 0),
lastPacket,
eta: getETA(),
eta: calculateETA(),
startDownload,
pauseDownload,
resumeDownload,

View File

@ -8,7 +8,8 @@ import urllib.parse
torrent_port = sys.argv[1]
http_port = sys.argv[2]
initial_download = json.loads(urllib.parse.unquote(sys.argv[3]))
rpc_password = sys.argv[3]
initial_download = json.loads(urllib.parse.unquote(sys.argv[4]))
class Downloader:
def __init__(self):
@ -67,8 +68,15 @@ downloader = Downloader()
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
class Handler(BaseHTTPRequestHandler):
rpc_password_header = 'x-hydra-rpc-password'
def do_GET(self):
if self.path == "/status":
if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401)
self.end_headers()
return
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
@ -82,6 +90,11 @@ class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path == "/action":
if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401)
self.end_headers()
return
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))