fix(integrations): use Unifi default port 8443 when no port is given
The node-unifi library hardcodes https:// in its base URL and the Unifi controller API only speaks HTTPS. When a user adds the integration with a bare http:// or https:// URL, getPortFromUrl returned 80 or 443, and the library attempted a TLS handshake on the wrong port. The server's plain HTTP response caused 'EPROTO ... wrong version number'. Derive the port from the user's explicit port, or fall back to 8443 (the integration's defaultPort). Closes #6207.
This commit is contained in:
1 parent
5b91687912
commit
8cd9e8c826
2 files changed
+119
-2
No files matched your search
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
vi.hoisted(() => {
|
||||
process.env.SKIP_ENV_VALIDATION = "true";
|
||||
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
|
||||
process.env.ENABLE_DNS_CACHING = "false";
|
||||
});
|
||||
|
||||
const controllerCtor = vi.fn();
|
||||
|
||||
vi.mock("@homarr/redis", () => ({
|
||||
createGetSetChannel: () => ({
|
||||
getAsync: vi.fn().mockResolvedValue(null),
|
||||
setAsync: vi.fn().mockResolvedValue(undefined),
|
||||
removeAsync: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/logs", () => ({
|
||||
createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
|
||||
ErrorWithMetadata: class extends Error {},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/http", () => ({
|
||||
createCustomCheckServerIdentity: () => (() => undefined) as never,
|
||||
fetchWithTrustedCertificatesAsync: vi.fn(),
|
||||
createAxiosCertificateInstanceAsync: vi.fn().mockResolvedValue({}),
|
||||
createCertificateAgentAsync: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", () => ({
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]),
|
||||
getAllTrustedCertificatesAsync: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock("axios", () => ({
|
||||
default: {
|
||||
create: vi.fn().mockReturnValue({}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("http-cookie-agent/http", () => ({
|
||||
HttpCookieAgent: class {},
|
||||
HttpsCookieAgent: class {},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/node-unifi", () => {
|
||||
class FakeController {
|
||||
public options: Record<string, unknown>;
|
||||
constructor(options: Record<string, unknown>) {
|
||||
this.options = options;
|
||||
controllerCtor(options);
|
||||
}
|
||||
public async login(): Promise<true> {
|
||||
return true;
|
||||
}
|
||||
public async getSitesStats(): Promise<unknown[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return { default: { Controller: FakeController } };
|
||||
});
|
||||
|
||||
import { UnifiControllerIntegration } from "../unifi-controller-integration";
|
||||
|
||||
const createIntegration = (url: string) =>
|
||||
new UnifiControllerIntegration({
|
||||
id: "test-unifi",
|
||||
name: "Test Unifi",
|
||||
url,
|
||||
externalUrl: null,
|
||||
decryptedSecrets: [
|
||||
{ kind: "username", value: "admin" },
|
||||
{ kind: "password", value: "secret" },
|
||||
],
|
||||
});
|
||||
|
||||
describe("UnifiControllerIntegration port resolution", () => {
|
||||
test("http://192.168.1.1 falls back to the 8443 default instead of 80", async () => {
|
||||
controllerCtor.mockClear();
|
||||
await createIntegration("http://192.168.1.1").getNetworkSummaryAsync();
|
||||
|
||||
expect(controllerCtor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ host: "192.168.1.1", port: 8443 }),
|
||||
);
|
||||
});
|
||||
|
||||
test("https://controller.lan:8443 keeps the user-specified port", async () => {
|
||||
controllerCtor.mockClear();
|
||||
await createIntegration("https://controller.lan:8443").getNetworkSummaryAsync();
|
||||
|
||||
expect(controllerCtor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ host: "controller.lan", port: 8443 }),
|
||||
);
|
||||
});
|
||||
|
||||
test("https://192.168.1.1:8443 keeps the user-specified port", async () => {
|
||||
controllerCtor.mockClear();
|
||||
await createIntegration("https://192.168.1.1:8443").getNetworkSummaryAsync();
|
||||
|
||||
expect(controllerCtor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ host: "192.168.1.1", port: 8443 }),
|
||||
);
|
||||
});
|
||||
|
||||
test("an unusual port like 10443 is honored verbatim", async () => {
|
||||
controllerCtor.mockClear();
|
||||
await createIntegration("https://192.168.1.1:10443").getNetworkSummaryAsync();
|
||||
|
||||
expect(controllerCtor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ host: "192.168.1.1", port: 10443 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import type tls from "node:tls";
|
||||
import axios from "axios";
|
||||
import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http";
|
||||
|
||||
import { getPortFromUrl } from "@homarr/common";
|
||||
import {
|
||||
getAllTrustedCertificatesAsync,
|
||||
getTrustedCertificateHostnamesAsync,
|
||||
@@ -62,6 +61,10 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
|
||||
checkServerIdentity: typeof tls.checkServerIdentity;
|
||||
}) {
|
||||
const url = new URL(this.integration.url);
|
||||
// ponytail: node-unifi hardcodes https:// in its base URL and never serves HTTP.
|
||||
// Respect the user's explicit port; fall back to 8443 (the integration's defaultPort)
|
||||
// so a bare "http://192.168.1.1" maps to https://192.168.1.1:8443 instead of 80.
|
||||
const port = url.port ? Number(url.port) : 8443;
|
||||
const certificateOptions = options ?? {
|
||||
ca: await getAllTrustedCertificatesAsync(),
|
||||
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
|
||||
@@ -69,7 +72,7 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
|
||||
|
||||
const client = new Unifi.Controller({
|
||||
host: url.hostname,
|
||||
port: getPortFromUrl(url),
|
||||
port,
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
createAxiosInstance({ cookies }) {
|
||||
|
||||
Reference in new issue
Block a user