mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
feat: adding authorization to rpc
This commit is contained in:
parent
96e96cd8aa
commit
363bcf16a4
121
src/main/services/download/http-download.ts
Normal file
121
src/main/services/download/http-download.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
];
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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'))
|
||||
|
Loading…
Reference in New Issue
Block a user