feat(certificates): handle self signed certificates (#1951)

* wip: add page and loading of certificates in folder

* wip: add certificate addition and removal

* feat: add removal ui for certificates

* feat: migrate integrations to fetch or agent with trusted certificates

* fix: lock file issues

* fix: typecheck issue

* fix: inconsistent package versions

* chore: address pull request feedback

* fix: add missing navigation item and restrict access to page

* chore: address pull request feedback

* fix: inconsistent undici dependency version

* fix: inconsistent undici dependency version
This commit is contained in:
Meier Lukas
2025-01-17 00:08:40 +01:00
committed by GitHub
parent b10b2013af
commit 8c36c3e36b
47 changed files with 737 additions and 122 deletions

View File

@@ -1,6 +1,8 @@
import { Deluge } from "@ctrl/deluge";
import dayjs from "dayjs";
import { createCertificateAgentAsync } from "@homarr/certificates/server";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -8,13 +10,13 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
export class DelugeIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
const client = this.getClient();
const client = await this.getClientAsync();
await client.login();
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "torrent";
const client = this.getClient();
const client = await this.getClientAsync();
const {
stats: { download_rate, upload_rate },
torrents: rawTorrents,
@@ -57,7 +59,7 @@ export class DelugeIntegration extends DownloadClientIntegration {
}
public async pauseQueueAsync() {
const client = this.getClient();
const client = await this.getClientAsync();
const store = (await client.listTorrents()).result.torrents;
await Promise.all(
Object.entries(store).map(async ([id]) => {
@@ -67,11 +69,12 @@ export class DelugeIntegration extends DownloadClientIntegration {
}
public async pauseItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.getClient().pauseTorrent(id);
const client = await this.getClientAsync();
await client.pauseTorrent(id);
}
public async resumeQueueAsync() {
const client = this.getClient();
const client = await this.getClientAsync();
const store = (await client.listTorrents()).result.torrents;
await Promise.all(
Object.entries(store).map(async ([id]) => {
@@ -81,17 +84,20 @@ export class DelugeIntegration extends DownloadClientIntegration {
}
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.getClient().resumeTorrent(id);
const client = await this.getClientAsync();
await client.resumeTorrent(id);
}
public async deleteItemAsync({ id }: DownloadClientItem, fromDisk: boolean): Promise<void> {
await this.getClient().removeTorrent(id, fromDisk);
const client = await this.getClientAsync();
await client.removeTorrent(id, fromDisk);
}
private getClient() {
private async getClientAsync() {
return new Deluge({
baseUrl: this.url("/").toString(),
password: this.getSecretValue("password"),
dispatcher: await createCertificateAgentAsync(),
});
}

View File

@@ -1,5 +1,7 @@
import dayjs from "dayjs";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -96,7 +98,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
const password = this.getSecretValue("password");
const url = this.url(`/${username}:${password}/jsonrpc`);
const body = JSON.stringify({ method, params });
return await fetch(url, { method: "POST", body })
return await fetchWithTrustedCertificatesAsync(url, { method: "POST", body })
.then(async (response) => {
if (!response.ok) {
throw new Error(response.statusText);

View File

@@ -1,6 +1,8 @@
import { QBittorrent } from "@ctrl/qbittorrent";
import dayjs from "dayjs";
import { createCertificateAgentAsync } from "@homarr/certificates/server";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -8,13 +10,13 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
export class QBitTorrentIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
const client = this.getClient();
const client = await this.getClientAsync();
await client.login();
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "torrent";
const client = this.getClient();
const client = await this.getClientAsync();
const torrents = await client.listTorrents();
const rates = torrents.reduce(
({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }),
@@ -50,30 +52,36 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
}
public async pauseQueueAsync() {
await this.getClient().pauseTorrent("all");
const client = await this.getClientAsync();
await client.pauseTorrent("all");
}
public async pauseItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.getClient().pauseTorrent(id);
const client = await this.getClientAsync();
await client.pauseTorrent(id);
}
public async resumeQueueAsync() {
await this.getClient().resumeTorrent("all");
const client = await this.getClientAsync();
await client.resumeTorrent("all");
}
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.getClient().resumeTorrent(id);
const client = await this.getClientAsync();
await client.resumeTorrent(id);
}
public async deleteItemAsync({ id }: DownloadClientItem, fromDisk: boolean): Promise<void> {
await this.getClient().removeTorrent(id, fromDisk);
const client = await this.getClientAsync();
await client.removeTorrent(id, fromDisk);
}
private getClient() {
private async getClientAsync() {
return new QBittorrent({
baseUrl: this.url("/").toString(),
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
dispatcher: await createCertificateAgentAsync(),
});
}

View File

@@ -1,6 +1,8 @@
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -106,12 +108,12 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
apikey: this.getSecretValue("apiKey"),
});
return await fetch(url)
return await fetchWithTrustedCertificatesAsync(url)
.then((response) => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json() as Promise<unknown>;
return response.json();
})
.catch((error) => {
if (error instanceof Error) {

View File

@@ -1,6 +1,8 @@
import { Transmission } from "@ctrl/transmission";
import dayjs from "dayjs";
import { createCertificateAgentAsync } from "@homarr/certificates/server";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -8,12 +10,13 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
export class TransmissionIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
await this.getClient().getSession();
const client = await this.getClientAsync();
await client.getSession();
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "torrent";
const client = this.getClient();
const client = await this.getClientAsync();
const { torrents } = (await client.listTorrents()).arguments;
const rates = torrents.reduce(
({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }),
@@ -47,34 +50,38 @@ export class TransmissionIntegration extends DownloadClientIntegration {
}
public async pauseQueueAsync() {
const client = this.getClient();
const client = await this.getClientAsync();
const ids = (await client.listTorrents()).arguments.torrents.map(({ hashString }) => hashString);
await this.getClient().pauseTorrent(ids);
await client.pauseTorrent(ids);
}
public async pauseItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.getClient().pauseTorrent(id);
const client = await this.getClientAsync();
await client.pauseTorrent(id);
}
public async resumeQueueAsync() {
const client = this.getClient();
const client = await this.getClientAsync();
const ids = (await client.listTorrents()).arguments.torrents.map(({ hashString }) => hashString);
await this.getClient().resumeTorrent(ids);
await client.resumeTorrent(ids);
}
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.getClient().resumeTorrent(id);
const client = await this.getClientAsync();
await client.resumeTorrent(id);
}
public async deleteItemAsync({ id }: DownloadClientItem, fromDisk: boolean): Promise<void> {
await this.getClient().removeTorrent(id, fromDisk);
const client = await this.getClientAsync();
await client.removeTorrent(id, fromDisk);
}
private getClient() {
private async getClientAsync() {
return new Transmission({
baseUrl: this.url("/").toString(),
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
dispatcher: await createCertificateAgentAsync(),
});
}