fix(nextcloud): integration ignores trusted certificates (#3893)

This commit is contained in:
Meier Lukas
2025-08-20 20:12:13 +02:00
committed by GitHub
parent afdf9d9c0f
commit 498acdcdc1
3 changed files with 56 additions and 10 deletions

View File

@@ -0,0 +1,44 @@
import { FetchError } from "node-fetch";
import { logger } from "@homarr/log";
import { RequestError } from "../request-error";
import type { AnyRequestError } from "../request-error";
import type { ResponseError } from "../response-error";
import { matchErrorCode } from "./fetch-http-error-handler";
import { HttpErrorHandler } from "./http-error-handler";
/**
* node-fetch was a defacto standard to use fetch in nodejs.
*
* It is for example used within the cross-fetch package which is used in tsdav.
*/
export class NodeFetchHttpErrorHandler extends HttpErrorHandler {
constructor(private type = "node-fetch") {
super();
}
handleRequestError(error: unknown): AnyRequestError | undefined {
if (!(error instanceof FetchError)) return undefined;
if (error.code === undefined) return undefined;
logger.debug(`Received ${this.type} request error`, {
code: error.code,
message: error.message,
});
const requestErrorInput = matchErrorCode(error.code);
if (!requestErrorInput) return undefined;
return new RequestError(requestErrorInput, {
cause: error,
});
}
/**
* Response errors do not exist for fetch as it does not throw errors for non successful responses.
*/
handleResponseError(_: unknown): ResponseError | undefined {
return undefined;
}
}

View File

@@ -2,12 +2,12 @@ import { logger } from "@homarr/log";
import type { AnyRequestError } from "../request-error"; import type { AnyRequestError } from "../request-error";
import { ResponseError } from "../response-error"; import { ResponseError } from "../response-error";
import { FetchHttpErrorHandler } from "./fetch-http-error-handler";
import { HttpErrorHandler } from "./http-error-handler"; import { HttpErrorHandler } from "./http-error-handler";
import { NodeFetchHttpErrorHandler } from "./node-fetch-http-error-handler";
export class TsdavHttpErrorHandler extends HttpErrorHandler { export class TsdavHttpErrorHandler extends HttpErrorHandler {
handleRequestError(error: unknown): AnyRequestError | undefined { handleRequestError(error: unknown): AnyRequestError | undefined {
return new FetchHttpErrorHandler("tsdav").handleRequestError(error); return new NodeFetchHttpErrorHandler("tsdav").handleRequestError(error);
} }
handleResponseError(error: unknown): ResponseError | undefined { handleResponseError(error: unknown): ResponseError | undefined {
@@ -16,7 +16,7 @@ export class TsdavHttpErrorHandler extends HttpErrorHandler {
// https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86 // https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86
if (error.message !== "Invalid credentials") return undefined; if (error.message !== "Invalid credentials") return undefined;
logger.debug("Received Tsdav response error", { logger.debug("Received tsdav response error", {
status: 401, status: 401,
}); });

View File

@@ -1,8 +1,9 @@
import type { Agent } from "https";
import type { RequestInit as NodeFetchRequestInit } from "node-fetch";
import * as ical from "node-ical"; import * as ical from "node-ical";
import { DAVClient } from "tsdav"; import { DAVClient } from "tsdav";
import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici";
import { createCertificateAgentAsync } from "@homarr/certificates/server"; import { createHttpsAgentAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { HandleIntegrationErrors } from "../base/errors/decorator"; import { HandleIntegrationErrors } from "../base/errors/decorator";
@@ -16,7 +17,7 @@ import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) @HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
export class NextcloudIntegration extends Integration implements ICalendarIntegration { export class NextcloudIntegration extends Integration implements ICalendarIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.createCalendarClientAsync(input.dispatcher); const client = await this.createCalendarClientAsync(await createHttpsAgentAsync(input.options));
await client.login(); await client.login();
return { success: true }; return { success: true };
@@ -80,7 +81,7 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
}); });
} }
private async createCalendarClientAsync(dispatcher?: Dispatcher) { private async createCalendarClientAsync(agent?: Agent) {
return new DAVClient({ return new DAVClient({
serverUrl: this.integration.url, serverUrl: this.integration.url,
credentials: { credentials: {
@@ -90,9 +91,10 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
authMethod: "Basic", authMethod: "Basic",
defaultAccountType: "caldav", defaultAccountType: "caldav",
fetchOptions: { fetchOptions: {
// We can use the undici options as the global fetch is used instead of the polyfilled. // tsdav is using cross-fetch which uses node-fetch for nodejs environments.
dispatcher: dispatcher ?? (await createCertificateAgentAsync()), // There is an agent property that is the same type as the http(s) agents of nodejs
} satisfies UndiciFetchRequestInit as RequestInit, agent: agent ?? (await createHttpsAgentAsync()),
} satisfies NodeFetchRequestInit as RequestInit,
}); });
} }
} }