Restore macOS direct LAN connections (#3071)
* fix(desktop): restore direct LAN connections Desktop 0.3.0 routed remote WebSockets through the Electron main process, causing previously working macOS LAN connections to fail with EHOSTUNREACH. Revert custom direct headers and restore the renderer WebSocket path used before the regression. * fix(app): replace host connections by identity Keep one connection per stable ID when TLS or password settings change. This preserves the current-main upsert invariant while the custom-header rollback removes the unrelated transport feature.
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import { expect, test } from "../../app/e2e/support/fixtures";
|
||||
import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app";
|
||||
import { getServerId } from "../../app/e2e/support/helpers/server-id";
|
||||
import {
|
||||
openAddHostFlow,
|
||||
selectHostConnectionType,
|
||||
toggleHostAdvanced,
|
||||
} from "../../app/e2e/support/helpers/settings";
|
||||
import { installDesktopRuntime } from "./support/runtime";
|
||||
|
||||
test("Electron users can add and remove direct connection custom headers", async ({ page }) => {
|
||||
await installDesktopRuntime(page, { serverId: getServerId() });
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
await openAddHostFlow(page);
|
||||
await selectHostConnectionType(page, "direct");
|
||||
await toggleHostAdvanced(page);
|
||||
|
||||
const addHeader = page.getByTestId("direct-header-add");
|
||||
await expect(addHeader).toBeVisible();
|
||||
await addHeader.click();
|
||||
|
||||
await page.getByTestId("direct-header-name-0").fill("X-Tenant");
|
||||
await page.getByTestId("direct-header-value-0").fill("acme");
|
||||
await expect(page.getByTestId("direct-header-name-0")).toHaveValue("X-Tenant");
|
||||
await expect(page.getByTestId("direct-header-value-0")).toHaveValue("acme");
|
||||
|
||||
await page.getByTestId("direct-header-remove-0").click();
|
||||
await expect(page.getByTestId("direct-header-name-0")).toHaveCount(0);
|
||||
await expect(page.getByTestId("direct-header-value-0")).toHaveCount(0);
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect, test } from "../../app/e2e/support/fixtures";
|
||||
import type { Page } from "@playwright/test";
|
||||
import {
|
||||
startIsolatedHostDaemon,
|
||||
type IsolatedHostDaemon,
|
||||
@@ -19,12 +18,11 @@ import {
|
||||
openPairDeviceFromHome,
|
||||
openRelaySecurityDocs,
|
||||
observePairingOfferRequests,
|
||||
preparePairingHost,
|
||||
prepareLocalPairingHost,
|
||||
reloadAndOpenPairDevice,
|
||||
retryRelayAndExpectFailure,
|
||||
switchPairDeviceToHost,
|
||||
} from "../../app/e2e/support/helpers/pair-device";
|
||||
import { installDesktopRuntime } from "./support/runtime";
|
||||
|
||||
interface RelayConfigDaemonClient {
|
||||
close(): Promise<void>;
|
||||
@@ -32,20 +30,6 @@ interface RelayConfigDaemonClient {
|
||||
patchDaemonConfig(config: { relay: { enabled: boolean } }): Promise<unknown>;
|
||||
}
|
||||
|
||||
async function prepareLocalPairingHost(
|
||||
page: Page,
|
||||
daemon: Parameters<typeof preparePairingHost>[1],
|
||||
additionalHosts: Parameters<typeof preparePairingHost>[2] = [],
|
||||
): Promise<void> {
|
||||
const endpoint = "port" in daemon ? `127.0.0.1:${daemon.port}` : daemon.endpoint;
|
||||
await installDesktopRuntime(page, {
|
||||
serverId: daemon.serverId,
|
||||
daemonListen: endpoint,
|
||||
manageBuiltInDaemon: false,
|
||||
});
|
||||
await preparePairingHost(page, daemon, additionalHosts);
|
||||
}
|
||||
|
||||
test.describe("local device relay pairing", () => {
|
||||
let relayOffDaemon: IsolatedHostDaemon;
|
||||
let relayEnableDaemon: IsolatedHostDaemon;
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { test, expect } from "../../app/e2e/support/fixtures";
|
||||
import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app";
|
||||
import { getServerId } from "../../app/e2e/support/helpers/server-id";
|
||||
import { installDesktopRuntime } from "./support/runtime";
|
||||
|
||||
test.describe("Settings sidebar scrolling", () => {
|
||||
test.use({ viewport: { width: 900, height: 260 } });
|
||||
|
||||
test("desktop drag region does not cover the scroll body", async ({ page }) => {
|
||||
await installDesktopRuntime(page, {
|
||||
serverId: getServerId(),
|
||||
manageBuiltInDaemon: true,
|
||||
await page.addInitScript(() => {
|
||||
window.paseoDesktop = {
|
||||
platform: "darwin",
|
||||
events: { on: () => () => {} },
|
||||
invoke: async (command: string) => {
|
||||
if (command === "get_desktop_settings") {
|
||||
return {
|
||||
releaseChannel: "stable",
|
||||
daemon: { manageBuiltInDaemon: true, keepRunningAfterQuit: true },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await gotoAppShell(page);
|
||||
|
||||
@@ -139,123 +139,8 @@ export async function installDesktopRuntime(
|
||||
let daemonRunning = true;
|
||||
let currentPid: number | null = cfg.daemonPid ?? null;
|
||||
let startCount = 0;
|
||||
let nextTransportSessionId = 0;
|
||||
const transportSessions = new Map<string, WebSocket>();
|
||||
const desktopEventHandlers = new Map<string, Set<(payload: unknown) => void>>();
|
||||
window.__desktopDaemonStartRequested = false;
|
||||
|
||||
function emitDesktopEvent(event: string, payload: unknown) {
|
||||
for (const handler of desktopEventHandlers.get(event) ?? []) {
|
||||
handler(payload);
|
||||
}
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
for (const byte of bytes) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function base64ToBytes(base64: string): Uint8Array {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function parseWebSocketTransportTarget(args?: Record<string, unknown>): {
|
||||
url: string;
|
||||
protocols: string[];
|
||||
} {
|
||||
const url = typeof args?.url === "string" ? args.url : "";
|
||||
const protocols = Array.isArray(args?.protocols)
|
||||
? args.protocols.filter((protocol): protocol is string => typeof protocol === "string")
|
||||
: [];
|
||||
const headers =
|
||||
args?.headers && typeof args.headers === "object"
|
||||
? Object.entries(args.headers as Record<string, unknown>)
|
||||
: [];
|
||||
const passwordProtocol = protocols.find((protocol) => protocol.startsWith("paseo.bearer."));
|
||||
const supportsHeaders =
|
||||
headers.length === 0 ||
|
||||
(headers.length === 1 &&
|
||||
headers[0][0].toLowerCase() === "authorization" &&
|
||||
typeof headers[0][1] === "string" &&
|
||||
passwordProtocol !== undefined &&
|
||||
headers[0][1] === `Bearer ${passwordProtocol.slice("paseo.bearer.".length)}`);
|
||||
|
||||
if (!url || !supportsHeaders) {
|
||||
throw new Error("The browser-backed desktop E2E transport cannot attach custom headers.");
|
||||
}
|
||||
return { url, protocols };
|
||||
}
|
||||
|
||||
async function openWebSocketTransport(args?: Record<string, unknown>): Promise<string> {
|
||||
const target = parseWebSocketTransportTarget(args);
|
||||
const sessionId = `e2e-websocket-session-${++nextTransportSessionId}`;
|
||||
const socket = new WebSocket(target.url, target.protocols);
|
||||
socket.binaryType = "arraybuffer";
|
||||
transportSessions.set(sessionId, socket);
|
||||
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
let opened = false;
|
||||
|
||||
socket.addEventListener("open", () => {
|
||||
opened = true;
|
||||
resolve(sessionId);
|
||||
emitDesktopEvent("local-daemon-transport-event", { sessionId, kind: "open" });
|
||||
});
|
||||
socket.addEventListener("message", async (event) => {
|
||||
if (typeof event.data === "string") {
|
||||
emitDesktopEvent("local-daemon-transport-event", {
|
||||
sessionId,
|
||||
kind: "message",
|
||||
text: event.data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const buffer =
|
||||
event.data instanceof ArrayBuffer
|
||||
? event.data
|
||||
: await (event.data as Blob).arrayBuffer();
|
||||
emitDesktopEvent("local-daemon-transport-event", {
|
||||
sessionId,
|
||||
kind: "message",
|
||||
binaryBase64: bytesToBase64(new Uint8Array(buffer)),
|
||||
});
|
||||
});
|
||||
socket.addEventListener("close", (event) => {
|
||||
transportSessions.delete(sessionId);
|
||||
if (!opened) {
|
||||
reject(new Error("Desktop E2E WebSocket closed before opening."));
|
||||
return;
|
||||
}
|
||||
emitDesktopEvent("local-daemon-transport-event", {
|
||||
sessionId,
|
||||
kind: "close",
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
});
|
||||
});
|
||||
socket.addEventListener("error", () => {
|
||||
if (!opened) {
|
||||
transportSessions.delete(sessionId);
|
||||
reject(new Error("Desktop E2E WebSocket failed to open."));
|
||||
return;
|
||||
}
|
||||
emitDesktopEvent("local-daemon-transport-event", {
|
||||
sessionId,
|
||||
kind: "error",
|
||||
error: "Desktop E2E WebSocket transport error.",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildDaemonStatus() {
|
||||
return {
|
||||
serverId: cfg.serverId,
|
||||
@@ -290,55 +175,6 @@ export async function installDesktopRuntime(
|
||||
}
|
||||
}
|
||||
|
||||
function patchDesktopSettings(args?: Record<string, unknown>) {
|
||||
const daemon = args?.daemon;
|
||||
if (
|
||||
daemon !== null &&
|
||||
typeof daemon === "object" &&
|
||||
"manageBuiltInDaemon" in daemon &&
|
||||
typeof daemon.manageBuiltInDaemon === "boolean"
|
||||
) {
|
||||
manageDaemon = daemon.manageBuiltInDaemon;
|
||||
}
|
||||
return {
|
||||
releaseChannel: "stable",
|
||||
daemon: { manageBuiltInDaemon: manageDaemon, keepRunningAfterQuit: true },
|
||||
};
|
||||
}
|
||||
|
||||
function sendWebSocketTransportMessage(args?: Record<string, unknown>): null {
|
||||
const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
|
||||
const socket = transportSessions.get(sessionId);
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
throw new Error(`Desktop E2E WebSocket session is not open: ${sessionId}`);
|
||||
}
|
||||
if (typeof args?.text === "string") {
|
||||
socket.send(args.text);
|
||||
return null;
|
||||
}
|
||||
if (typeof args?.binaryBase64 === "string") {
|
||||
socket.send(base64ToBytes(args.binaryBase64));
|
||||
return null;
|
||||
}
|
||||
throw new Error("Desktop E2E WebSocket send requires a payload.");
|
||||
}
|
||||
|
||||
function closeWebSocketTransport(args?: Record<string, unknown>): null {
|
||||
const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
|
||||
transportSessions.get(sessionId)?.close();
|
||||
transportSessions.delete(sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const webSocketTransportCommandHandlers: Record<
|
||||
string,
|
||||
(args?: Record<string, unknown>) => unknown
|
||||
> = {
|
||||
open_websocket_daemon_transport: openWebSocketTransport,
|
||||
send_local_daemon_transport_message: sendWebSocketTransportMessage,
|
||||
close_local_daemon_transport: closeWebSocketTransport,
|
||||
};
|
||||
|
||||
const desktopBridge: {
|
||||
platform: string;
|
||||
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
@@ -347,10 +183,7 @@ export async function installDesktopRuntime(
|
||||
open: (options?: Record<string, unknown>) => Promise<string | string[] | null>;
|
||||
};
|
||||
getPendingOpenProject: () => Promise<string | null>;
|
||||
opener: { openUrl: (url: string) => Promise<void> };
|
||||
events: {
|
||||
on: (event: string, handler: (payload: unknown) => void) => Promise<() => void>;
|
||||
};
|
||||
events: { on: () => Promise<() => void> };
|
||||
editor?: {
|
||||
listTargets: () => Promise<DesktopEditorTargetConfig[]>;
|
||||
openTarget: (input: DesktopEditorOpenRecord) => Promise<void>;
|
||||
@@ -406,7 +239,19 @@ export async function installDesktopRuntime(
|
||||
}
|
||||
|
||||
if (command === "patch_desktop_settings") {
|
||||
return patchDesktopSettings(args);
|
||||
const daemon = args?.daemon;
|
||||
if (
|
||||
daemon !== null &&
|
||||
typeof daemon === "object" &&
|
||||
"manageBuiltInDaemon" in daemon &&
|
||||
typeof daemon.manageBuiltInDaemon === "boolean"
|
||||
) {
|
||||
manageDaemon = daemon.manageBuiltInDaemon;
|
||||
}
|
||||
return {
|
||||
releaseChannel: "stable",
|
||||
daemon: { manageBuiltInDaemon: manageDaemon, keepRunningAfterQuit: true },
|
||||
};
|
||||
}
|
||||
|
||||
if (command === "stop_desktop_daemon") {
|
||||
@@ -419,11 +264,6 @@ export async function installDesktopRuntime(
|
||||
return startDesktopDaemon();
|
||||
}
|
||||
|
||||
const transportCommandHandler = webSocketTransportCommandHandlers[command];
|
||||
if (transportCommandHandler) {
|
||||
return await transportCommandHandler(args);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
dialog: {
|
||||
@@ -440,24 +280,7 @@ export async function installDesktopRuntime(
|
||||
},
|
||||
},
|
||||
getPendingOpenProject: async () => null,
|
||||
opener: {
|
||||
openUrl: async (url) => {
|
||||
localStorage.setItem("@paseo:e2e-opened-url", url);
|
||||
},
|
||||
},
|
||||
events: {
|
||||
on: async (event, handler) => {
|
||||
const handlers = desktopEventHandlers.get(event) ?? new Set();
|
||||
handlers.add(handler);
|
||||
desktopEventHandlers.set(event, handlers);
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (handlers.size === 0) {
|
||||
desktopEventHandlers.delete(event);
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
events: { on: async () => () => undefined },
|
||||
};
|
||||
|
||||
if (cfg.editorTargets) {
|
||||
|
||||
Reference in New Issue
Block a user