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 { Game } from "@main/entity";
|
||||||
import { RealDebridClient } from "../real-debrid";
|
import { RealDebridClient } from "../real-debrid";
|
||||||
import axios, { AxiosProgressEvent } from "axios";
|
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { calculateETA } from "./helpers";
|
import { calculateETA } from "./helpers";
|
||||||
import { DownloadProgress } from "@types";
|
import { DownloadProgress } from "@types";
|
||||||
|
import { HttpDownload } from "./http-download";
|
||||||
|
|
||||||
export class RealDebridDownloader {
|
export class RealDebridDownloader {
|
||||||
private static downloadingGame: Game | null = null;
|
private static downloadingGame: Game | null = null;
|
||||||
|
|
||||||
private static realDebridTorrentId: string | null = null;
|
private static realDebridTorrentId: string | null = null;
|
||||||
private static lastProgressEvent: AxiosProgressEvent | null = null;
|
private static httpDownload: HttpDownload | null = null;
|
||||||
private static abortController: AbortController | null = null;
|
|
||||||
|
|
||||||
private static async getRealDebridDownloadUrl() {
|
private static async getRealDebridDownloadUrl() {
|
||||||
if (this.realDebridTorrentId) {
|
if (this.realDebridTorrentId) {
|
||||||
@ -38,13 +35,15 @@ export class RealDebridDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getStatus() {
|
public static async getStatus() {
|
||||||
if (this.lastProgressEvent) {
|
const lastProgressEvent = this.httpDownload?.lastProgressEvent;
|
||||||
|
|
||||||
|
if (lastProgressEvent) {
|
||||||
await gameRepository.update(
|
await gameRepository.update(
|
||||||
{ id: this.downloadingGame!.id },
|
{ id: this.downloadingGame!.id },
|
||||||
{
|
{
|
||||||
bytesDownloaded: this.lastProgressEvent.loaded,
|
bytesDownloaded: lastProgressEvent.loaded,
|
||||||
fileSize: this.lastProgressEvent.total,
|
fileSize: lastProgressEvent.total,
|
||||||
progress: this.lastProgressEvent.progress,
|
progress: lastProgressEvent.progress,
|
||||||
status: "active",
|
status: "active",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -52,19 +51,19 @@ export class RealDebridDownloader {
|
|||||||
const progress = {
|
const progress = {
|
||||||
numPeers: 0,
|
numPeers: 0,
|
||||||
numSeeds: 0,
|
numSeeds: 0,
|
||||||
downloadSpeed: this.lastProgressEvent.rate,
|
downloadSpeed: lastProgressEvent.rate,
|
||||||
timeRemaining: calculateETA(
|
timeRemaining: calculateETA(
|
||||||
this.lastProgressEvent.total ?? 0,
|
lastProgressEvent.total ?? 0,
|
||||||
this.lastProgressEvent.loaded,
|
lastProgressEvent.loaded,
|
||||||
this.lastProgressEvent.rate ?? 0
|
lastProgressEvent.rate ?? 0
|
||||||
),
|
),
|
||||||
isDownloadingMetadata: false,
|
isDownloadingMetadata: false,
|
||||||
isCheckingFiles: false,
|
isCheckingFiles: false,
|
||||||
progress: this.lastProgressEvent.progress,
|
progress: lastProgressEvent.progress,
|
||||||
gameId: this.downloadingGame!.id,
|
gameId: this.downloadingGame!.id,
|
||||||
} as DownloadProgress;
|
} as DownloadProgress;
|
||||||
|
|
||||||
if (this.lastProgressEvent.progress === 1) {
|
if (lastProgressEvent.progress === 1) {
|
||||||
this.pauseDownload();
|
this.pauseDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,13 +101,8 @@ export class RealDebridDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload() {
|
||||||
if (this.abortController) {
|
this.httpDownload?.pauseDownload();
|
||||||
this.abortController.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.abortController = null;
|
|
||||||
this.realDebridTorrentId = null;
|
this.realDebridTorrentId = null;
|
||||||
this.lastProgressEvent = null;
|
|
||||||
this.downloadingGame = null;
|
this.downloadingGame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,30 +114,12 @@ export class RealDebridDownloader {
|
|||||||
|
|
||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
this.realDebridTorrentId = null;
|
this.realDebridTorrentId = null;
|
||||||
this.abortController = new AbortController();
|
this.httpDownload = new HttpDownload(downloadUrl, game!.downloadPath!);
|
||||||
|
this.httpDownload.startDownload();
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload() {
|
static cancelDownload() {
|
||||||
return this.pauseDownload();
|
return this.httpDownload?.cancelDownload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import cp from "node:child_process";
|
import cp from "node:child_process";
|
||||||
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { app, dialog } from "electron";
|
import { app, dialog } from "electron";
|
||||||
import type { StartDownloadPayload } from "./types";
|
import type { StartDownloadPayload } from "./types";
|
||||||
@ -12,11 +13,13 @@ const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
|||||||
|
|
||||||
export const BITTORRENT_PORT = "5881";
|
export const BITTORRENT_PORT = "5881";
|
||||||
export const RPC_PORT = "8084";
|
export const RPC_PORT = "8084";
|
||||||
|
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
export const startTorrentClient = (args: StartDownloadPayload) => {
|
export const startTorrentClient = (args: StartDownloadPayload) => {
|
||||||
const commonArgs = [
|
const commonArgs = [
|
||||||
BITTORRENT_PORT,
|
BITTORRENT_PORT,
|
||||||
RPC_PORT,
|
RPC_PORT,
|
||||||
|
RPC_PASSWORD,
|
||||||
encodeURIComponent(JSON.stringify(args)),
|
encodeURIComponent(JSON.stringify(args)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import cp from "node:child_process";
|
import cp from "node:child_process";
|
||||||
|
|
||||||
import { Game } from "@main/entity";
|
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 { gameRepository } from "@main/repository";
|
||||||
import { DownloadProgress } from "@types";
|
import { DownloadProgress } from "@types";
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||||
@ -21,6 +21,9 @@ export class TorrentDownloader {
|
|||||||
|
|
||||||
private static rpc = axios.create({
|
private static rpc = axios.create({
|
||||||
baseURL: `http://localhost:${RPC_PORT}`,
|
baseURL: `http://localhost:${RPC_PORT}`,
|
||||||
|
headers: {
|
||||||
|
"x-hydra-rpc-password": RPC_PASSWORD,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
private static spawn(args: StartDownloadPayload) {
|
private static spawn(args: StartDownloadPayload) {
|
||||||
|
@ -22,13 +22,14 @@ export function useDownload() {
|
|||||||
);
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const startDownload = (payload: StartGameDownloadPayload) =>
|
const startDownload = (payload: StartGameDownloadPayload) => {
|
||||||
window.electron.startGameDownload(payload).then((game) => {
|
|
||||||
dispatch(clearDownload());
|
dispatch(clearDownload());
|
||||||
|
window.electron.startGameDownload(payload).then((game) => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const pauseDownload = async (gameId: number) => {
|
const pauseDownload = async (gameId: number) => {
|
||||||
await window.electron.pauseGameDownload(gameId);
|
await window.electron.pauseGameDownload(gameId);
|
||||||
@ -65,7 +66,7 @@ export function useDownload() {
|
|||||||
updateLibrary();
|
updateLibrary();
|
||||||
});
|
});
|
||||||
|
|
||||||
const getETA = () => {
|
const calculateETA = () => {
|
||||||
if (!lastPacket || lastPacket.timeRemaining < 0) return "";
|
if (!lastPacket || lastPacket.timeRemaining < 0) return "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -87,7 +88,7 @@ export function useDownload() {
|
|||||||
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
|
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
|
||||||
progress: formatDownloadProgress(lastPacket?.progress ?? 0),
|
progress: formatDownloadProgress(lastPacket?.progress ?? 0),
|
||||||
lastPacket,
|
lastPacket,
|
||||||
eta: getETA(),
|
eta: calculateETA(),
|
||||||
startDownload,
|
startDownload,
|
||||||
pauseDownload,
|
pauseDownload,
|
||||||
resumeDownload,
|
resumeDownload,
|
||||||
|
@ -8,7 +8,8 @@ import urllib.parse
|
|||||||
|
|
||||||
torrent_port = sys.argv[1]
|
torrent_port = sys.argv[1]
|
||||||
http_port = sys.argv[2]
|
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:
|
class Downloader:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -67,8 +68,15 @@ downloader = Downloader()
|
|||||||
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
|
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
rpc_password_header = 'x-hydra-rpc-password'
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/status":
|
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_response(200)
|
||||||
self.send_header("Content-type", "application/json")
|
self.send_header("Content-type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@ -82,6 +90,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if self.path == "/action":
|
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'])
|
content_length = int(self.headers['Content-Length'])
|
||||||
post_data = self.rfile.read(content_length)
|
post_data = self.rfile.read(content_length)
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
data = json.loads(post_data.decode('utf-8'))
|
||||||
|
Loading…
Reference in New Issue
Block a user