diff --git a/src/Result.ts b/src/Result.ts index 47d410670..fe54ff613 100644 --- a/src/Result.ts +++ b/src/Result.ts @@ -3,7 +3,7 @@ export interface Result { isErr(): boolean; mapOk(mapper: (t: T) => R): Result; mapErr(mapper: (u: U) => R): Result; - unwrapOrThrow(): T; + unwrapOrThrow(errToThrow?: Error): T; unwrapErrOrThrow(): U; } @@ -26,11 +26,13 @@ class OkResult implements Result { return new OkResult(mapper(this.contents)); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars mapErr(mapper: (u: U) => R): Result { return this as unknown as Result; } - unwrapOrThrow(): T { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + unwrapOrThrow(errToThrow?: Error): T { return this.contents; } @@ -54,6 +56,7 @@ class ErrResult implements Result { return true; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars mapOk(mapper: (t: T) => R): Result { return this as unknown as Result; } @@ -62,8 +65,8 @@ class ErrResult implements Result { return new ErrResult(mapper(this.contents)); } - unwrapOrThrow(): T { - throw new Error("Tried to unwrap an ErrResult's error"); + unwrapOrThrow(errToThrow?: Error): T { + throw errToThrow ?? new Error("Tried to unwrap an ErrResult's error"); } unwrapErrOrThrow(): U { diff --git a/src/clients/QBittorrent.ts b/src/clients/QBittorrent.ts index ab0100698..36e24776e 100644 --- a/src/clients/QBittorrent.ts +++ b/src/clients/QBittorrent.ts @@ -10,6 +10,7 @@ import { Label, logger, logOnce } from "../logger.js"; import { Metafile } from "../parseTorrent.js"; import { getRuntimeConfig } from "../runtimeConfig.js"; import { Searchee } from "../searchee.js"; +import { extractCredentialsFromUrl } from "../utils.js"; import { TorrentClient } from "./TorrentClient.js"; const X_WWW_FORM_URLENCODED = { @@ -85,36 +86,21 @@ interface Category { } export default class QBittorrent implements TorrentClient { - url: URL; cookie: string; - constructor() { - const { qbittorrentUrl } = getRuntimeConfig(); - try { - this.url = new URL(`${qbittorrentUrl}/api/v2`); - } catch (e) { - throw new CrossSeedError("qBittorrent url must be percent-encoded"); - } - } - async login(): Promise { - const { origin, pathname, username, password } = this.url; - - let searchParams; - try { - searchParams = new URLSearchParams({ - username: decodeURIComponent(username), - password: decodeURIComponent(password), - }); - } catch (e) { - throw new CrossSeedError("qBittorrent url must be percent-encoded"); - } + const { qbittorrentUrl } = getRuntimeConfig(); + const { username, password, href } = extractCredentialsFromUrl( + qbittorrentUrl + ).unwrapOrThrow( + new CrossSeedError("qBittorrent url must be percent-encoded") + ); let response: Response; try { - response = await fetch(`${origin}${pathname}/auth/login`, { + response = await fetch(`${href}/api/v2/auth/login`, { method: "POST", - body: searchParams, + body: new URLSearchParams({ username, password }), }); } catch (e) { throw new CrossSeedError(`qBittorrent login failed: ${e.message}`); @@ -151,8 +137,10 @@ export default class QBittorrent implements TorrentClient { label: Label.QBITTORRENT, message: `Making request to ${path} with body ${body.toString()}`, }); - const { origin, pathname } = this.url; - const response = await fetch(`${origin}${pathname}${path}`, { + const { qbittorrentUrl } = getRuntimeConfig(); + const { href } = + extractCredentialsFromUrl(qbittorrentUrl).unwrapOrThrow(); + const response = await fetch(`${href}/api/v2${path}`, { method: "post", headers: { Cookie: this.cookie, ...headers }, body, diff --git a/src/clients/RTorrent.ts b/src/clients/RTorrent.ts index 9043731b9..af14be90a 100644 --- a/src/clients/RTorrent.ts +++ b/src/clients/RTorrent.ts @@ -9,7 +9,7 @@ import { Metafile } from "../parseTorrent.js"; import { Result, resultOf, resultOfErr } from "../Result.js"; import { getRuntimeConfig } from "../runtimeConfig.js"; import { File, Searchee } from "../searchee.js"; -import { wait } from "../utils.js"; +import { extractCredentialsFromUrl, wait } from "../utils.js"; import { TorrentClient } from "./TorrentClient.js"; interface LibTorrentResumeFileEntry { @@ -80,30 +80,25 @@ export default class RTorrent implements TorrentClient { constructor() { const { rtorrentRpcUrl } = getRuntimeConfig(); - try { - const { origin, username, password, protocol, pathname } = new URL( - rtorrentRpcUrl - ); + const { href, username, password } = extractCredentialsFromUrl( + rtorrentRpcUrl + ).unwrapOrThrow( + new CrossSeedError("rTorrent url must be percent-encoded") + ); - const clientCreator = - protocol === "https:" - ? xmlrpc.createSecureClient - : xmlrpc.createClient; + const clientCreator = + new URL(href).protocol === "https:" + ? xmlrpc.createSecureClient + : xmlrpc.createClient; - const shouldUseAuth = Boolean(username && password); + const shouldUseAuth = Boolean(username && password); - this.client = clientCreator({ - url: origin + pathname, - basic_auth: shouldUseAuth - ? { - user: decodeURIComponent(username), - pass: decodeURIComponent(password), - } - : undefined, - }); - } catch (e) { - throw new CrossSeedError("rTorrent url must be percent-encoded"); - } + this.client = clientCreator({ + url: href, + basic_auth: shouldUseAuth + ? { user: username, pass: password } + : undefined, + }); } private async methodCallP(method: string, args): Promise { diff --git a/src/clients/Transmission.ts b/src/clients/Transmission.ts index 8d11716ab..b20c45245 100644 --- a/src/clients/Transmission.ts +++ b/src/clients/Transmission.ts @@ -6,6 +6,7 @@ import { Metafile } from "../parseTorrent.js"; import { Result, resultOf, resultOfErr } from "../Result.js"; import { getRuntimeConfig } from "../runtimeConfig.js"; import { Searchee } from "../searchee.js"; +import { extractCredentialsFromUrl } from "../utils.js"; import { TorrentClient } from "./TorrentClient.js"; const XTransmissionSessionId = "X-Transmission-Session-Id"; @@ -46,8 +47,10 @@ export default class Transmission implements TorrentClient { ): Promise { const { transmissionRpcUrl } = getRuntimeConfig(); - const { username, password, origin, pathname } = new URL( + const { username, password, href } = extractCredentialsFromUrl( transmissionRpcUrl + ).unwrapOrThrow( + new CrossSeedError("Transmission rpc url must be percent-encoded") ); const headers = new Headers(); @@ -62,7 +65,7 @@ export default class Transmission implements TorrentClient { headers.set("Authorization", `Basic ${credentials}`); } - const response = await fetch(origin + pathname, { + const response = await fetch(href, { method: "POST", body: JSON.stringify({ method, arguments: args }), headers, diff --git a/src/utils.ts b/src/utils.ts index 8fd83ed6a..1cb408e8a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ import { SEASON_REGEX, VIDEO_EXTENSIONS, } from "./constants.js"; +import { Result, resultOf, resultOfErr } from "./Result.js"; export enum MediaType { EPISODE = "episode", @@ -93,3 +94,18 @@ export function fallback(...args: T[]): T { } return undefined; } + +export function extractCredentialsFromUrl( + url: string +): Result<{ username: string; password: string; href: string }, "invalid URL"> { + try { + const { origin, pathname, username, password } = new URL(url); + return resultOf({ + username: decodeURIComponent(username), + password: decodeURIComponent(password), + href: origin + pathname, + }); + } catch (e) { + return resultOfErr("invalid URL"); + } +}