mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor: centralize relay vs direct E2EE
This commit is contained in:
@@ -22,11 +22,11 @@ function buildCandidateUrls(daemon: HostProfile): string[] {
|
||||
};
|
||||
|
||||
const isLastKnownRelay = !!relayEndpoint && lastKnownGood === relayEndpoint;
|
||||
const directEndpoints = relayEndpoint
|
||||
? endpoints.filter((endpoint) => endpoint !== relayEndpoint)
|
||||
: endpoints;
|
||||
const directEndpoints = endpoints;
|
||||
|
||||
if (lastKnownGood && !isLastKnownRelay) {
|
||||
if (sessionId && relayEndpoint && isLastKnownRelay) {
|
||||
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
||||
} else if (lastKnownGood) {
|
||||
push(buildDaemonWebSocketUrl(lastKnownGood));
|
||||
}
|
||||
|
||||
@@ -35,9 +35,6 @@ function buildCandidateUrls(daemon: HostProfile): string[] {
|
||||
}
|
||||
|
||||
if (sessionId && relayEndpoint) {
|
||||
if (isLastKnownRelay) {
|
||||
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
||||
}
|
||||
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createContext, useCallback, useContext } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
buildDaemonWebSocketUrl,
|
||||
decodeOfferFragmentPayload,
|
||||
@@ -10,6 +9,10 @@ import {
|
||||
extractHostPortFromWebSocketUrl,
|
||||
normalizeHostPort,
|
||||
} from "@/utils/daemon-endpoints";
|
||||
import {
|
||||
ConnectionOfferV1Schema,
|
||||
type ConnectionOfferV1,
|
||||
} from "@server/shared/connection-offer";
|
||||
|
||||
const REGISTRY_STORAGE_KEY = "@paseo:daemon-registry";
|
||||
const LEGACY_SETTINGS_KEY = "@paseo:settings";
|
||||
@@ -41,15 +44,6 @@ type CreateHostInput = {
|
||||
|
||||
type UpdateHostInput = Partial<Omit<HostProfile, "id" | "createdAt">>;
|
||||
|
||||
const ConnectionOfferV1Schema = z.object({
|
||||
v: z.literal(1),
|
||||
sessionId: z.string().min(1),
|
||||
endpoints: z.array(z.string().min(1)).min(1),
|
||||
daemonPublicKeyB64: z.string().min(1),
|
||||
});
|
||||
|
||||
export type ConnectionOfferV1 = z.infer<typeof ConnectionOfferV1Schema>;
|
||||
|
||||
interface DaemonRegistryContextValue {
|
||||
daemons: HostProfile[];
|
||||
isLoading: boolean;
|
||||
@@ -134,7 +128,12 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
||||
const existing = readDaemons();
|
||||
const now = new Date().toISOString();
|
||||
const normalizedEndpoints = offer.endpoints.map((endpoint) => normalizeHostPort(endpoint));
|
||||
const relayEndpoint = normalizedEndpoints[normalizedEndpoints.length - 1];
|
||||
const relayEndpoint =
|
||||
offer.relay?.endpoint
|
||||
? normalizeHostPort(offer.relay.endpoint)
|
||||
: offer.relay === undefined && normalizedEndpoints.length > 0
|
||||
? normalizedEndpoints[normalizedEndpoints.length - 1]
|
||||
: null;
|
||||
|
||||
const matchIndex = existing.findIndex((daemon) => daemon.daemonPublicKeyB64 === offer.daemonPublicKeyB64);
|
||||
if (matchIndex !== -1) {
|
||||
@@ -142,7 +141,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
||||
...existing[matchIndex],
|
||||
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
||||
endpoints: normalizedEndpoints,
|
||||
relay: { endpoint: relayEndpoint, sessionId: offer.sessionId },
|
||||
relay: relayEndpoint ? { endpoint: relayEndpoint, sessionId: offer.sessionId } : null,
|
||||
updatedAt: now,
|
||||
};
|
||||
const next = [...existing];
|
||||
@@ -156,7 +155,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
||||
label: deriveLabelFromEndpoint(normalizedEndpoints[0] ?? "Unnamed Host"),
|
||||
endpoints: normalizedEndpoints,
|
||||
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
||||
relay: { endpoint: relayEndpoint, sessionId: offer.sessionId },
|
||||
relay: relayEndpoint ? { endpoint: relayEndpoint, sessionId: offer.sessionId } : null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
metadata: null,
|
||||
|
||||
@@ -13,15 +13,6 @@ type DaemonClientOptions = {
|
||||
daemonPublicKeyB64?: string;
|
||||
};
|
||||
|
||||
function isRelayWebSocketUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.searchParams.get("role") === "client" && parsed.searchParams.has("session");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function useDaemonClient(
|
||||
url: string,
|
||||
options: DaemonClientOptions = {}
|
||||
@@ -29,21 +20,18 @@ export function useDaemonClient(
|
||||
const client = useMemo(
|
||||
() => {
|
||||
const tauriTransportFactory = createTauriWebSocketTransportFactory();
|
||||
const relayConnection = isRelayWebSocketUrl(url);
|
||||
return new DaemonClientV2({
|
||||
url,
|
||||
suppressSendErrors: true,
|
||||
...(tauriTransportFactory
|
||||
? { transportFactory: tauriTransportFactory }
|
||||
: {}),
|
||||
...(relayConnection
|
||||
e2ee: options.daemonPublicKeyB64
|
||||
? {
|
||||
e2ee: {
|
||||
enabled: true,
|
||||
daemonPublicKeyB64: options.daemonPublicKeyB64,
|
||||
},
|
||||
enabled: true,
|
||||
daemonPublicKeyB64: options.daemonPublicKeyB64,
|
||||
}
|
||||
: {}),
|
||||
: undefined,
|
||||
});
|
||||
},
|
||||
[options.daemonPublicKeyB64, url]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./e2ee": "./src/e2ee.ts",
|
||||
"./node": "./src/node-adapter.ts",
|
||||
"./cloudflare": "./src/cloudflare-adapter.ts"
|
||||
},
|
||||
|
||||
7
packages/relay/src/e2ee.ts
Normal file
7
packages/relay/src/e2ee.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
createClientChannel,
|
||||
createDaemonChannel,
|
||||
EncryptedChannel,
|
||||
} from "./encrypted-channel.js";
|
||||
export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
createClientChannel,
|
||||
type EncryptedChannel,
|
||||
type Transport as RelayTransport,
|
||||
} from "@paseo/relay";
|
||||
} from "@paseo/relay/e2ee";
|
||||
|
||||
export interface Logger {
|
||||
debug(obj: object, msg?: string): void;
|
||||
@@ -319,14 +319,16 @@ export class DaemonClientV2 {
|
||||
createWebSocketTransportFactory(
|
||||
this.config.webSocketFactory ?? defaultWebSocketFactory
|
||||
);
|
||||
const transportFactory =
|
||||
this.config.e2ee?.enabled === true
|
||||
? createEncryptedTransportFactory(
|
||||
baseTransportFactory,
|
||||
this.config.e2ee?.daemonPublicKeyB64,
|
||||
this.logger
|
||||
)
|
||||
: baseTransportFactory;
|
||||
const shouldUseRelayE2ee =
|
||||
this.config.e2ee?.enabled === true &&
|
||||
isRelayClientWebSocketUrl(this.config.url);
|
||||
const transportFactory = shouldUseRelayE2ee
|
||||
? createRelayE2eeTransportFactory(
|
||||
baseTransportFactory,
|
||||
this.config.e2ee?.daemonPublicKeyB64,
|
||||
this.logger
|
||||
)
|
||||
: baseTransportFactory;
|
||||
const transport = transportFactory({ url: this.config.url, headers });
|
||||
this.transport = transport;
|
||||
|
||||
@@ -2342,7 +2344,7 @@ function createWebSocketTransportFactory(
|
||||
};
|
||||
}
|
||||
|
||||
function createEncryptedTransportFactory(
|
||||
function createRelayE2eeTransportFactory(
|
||||
baseFactory: DaemonTransportFactory,
|
||||
daemonPublicKeyB64: string | undefined,
|
||||
logger: Logger
|
||||
@@ -2357,6 +2359,18 @@ function createEncryptedTransportFactory(
|
||||
};
|
||||
}
|
||||
|
||||
function isRelayClientWebSocketUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return (
|
||||
parsed.searchParams.get("role") === "client" &&
|
||||
parsed.searchParams.has("session")
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createEncryptedTransport(
|
||||
base: DaemonTransport,
|
||||
daemonPublicKeyB64: string,
|
||||
|
||||
@@ -437,14 +437,13 @@ export async function createPaseoDaemon(
|
||||
const endpoints = buildOfferEndpoints({
|
||||
listenHost: listenTarget.host,
|
||||
port: listenTarget.port,
|
||||
relayEnabled,
|
||||
relayEndpoint,
|
||||
});
|
||||
|
||||
const offer = await createConnectionOfferV1({
|
||||
sessionId: connectionSessionId,
|
||||
endpoints,
|
||||
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
||||
relay: relayEnabled ? { endpoint: relayEndpoint } : null,
|
||||
});
|
||||
|
||||
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import os from "node:os";
|
||||
import { z } from "zod";
|
||||
|
||||
export const ConnectionOfferV1Schema = z.object({
|
||||
v: z.literal(1),
|
||||
sessionId: z.string().min(1),
|
||||
endpoints: z.array(z.string().min(1)).min(1),
|
||||
daemonPublicKeyB64: z.string().min(1),
|
||||
});
|
||||
|
||||
export type ConnectionOfferV1 = z.infer<typeof ConnectionOfferV1Schema>;
|
||||
import {
|
||||
ConnectionOfferV1Schema,
|
||||
type ConnectionOfferV1,
|
||||
} from "../shared/connection-offer.js";
|
||||
|
||||
type BuildOfferEndpointsArgs = {
|
||||
listenHost: string;
|
||||
port: number;
|
||||
relayEnabled: boolean;
|
||||
relayEndpoint: string;
|
||||
};
|
||||
|
||||
export function buildOfferEndpoints({
|
||||
listenHost,
|
||||
port,
|
||||
relayEnabled,
|
||||
relayEndpoint,
|
||||
}: BuildOfferEndpointsArgs): string[] {
|
||||
const endpoints: string[] = [];
|
||||
|
||||
@@ -41,10 +32,6 @@ export function buildOfferEndpoints({
|
||||
endpoints.push(`localhost:${port}`);
|
||||
endpoints.push(`127.0.0.1:${port}`);
|
||||
|
||||
if (relayEnabled) {
|
||||
endpoints.push(relayEndpoint);
|
||||
}
|
||||
|
||||
return dedupePreserveOrder(endpoints);
|
||||
}
|
||||
|
||||
@@ -52,12 +39,14 @@ export async function createConnectionOfferV1(args: {
|
||||
sessionId: string;
|
||||
endpoints: string[];
|
||||
daemonPublicKeyB64: string;
|
||||
relay?: { endpoint: string } | null;
|
||||
}): Promise<ConnectionOfferV1> {
|
||||
return ConnectionOfferV1Schema.parse({
|
||||
v: 1,
|
||||
sessionId: args.sessionId,
|
||||
endpoints: args.endpoints,
|
||||
daemonPublicKeyB64: args.daemonPublicKeyB64,
|
||||
relay: args.relay ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ describe("ConnectionOfferV1 (daemon E2E)", () => {
|
||||
sessionId: string;
|
||||
endpoints: string[];
|
||||
daemonPublicKeyB64: string;
|
||||
relay?: { endpoint: string } | null;
|
||||
};
|
||||
|
||||
expect(offer.v).toBe(1);
|
||||
@@ -97,7 +98,8 @@ describe("ConnectionOfferV1 (daemon E2E)", () => {
|
||||
expect(Array.isArray(offer.endpoints)).toBe(true);
|
||||
expect(offer.endpoints).toContain(`192.168.1.12:${daemon.port}`);
|
||||
expect(offer.endpoints).toContain(`localhost:${daemon.port}`);
|
||||
expect(offer.endpoints).toContain("relay.paseo.sh:443");
|
||||
expect(offer.endpoints).not.toContain("relay.paseo.sh:443");
|
||||
expect(offer.relay?.endpoint).toBe("relay.paseo.sh:443");
|
||||
expect(typeof offer.daemonPublicKeyB64).toBe("string");
|
||||
expect(offer.daemonPublicKeyB64.length).toBeGreaterThan(0);
|
||||
expect(() => Buffer.from(offer.daemonPublicKeyB64, "base64")).not.toThrow();
|
||||
@@ -167,9 +169,11 @@ describe("ConnectionOfferV1 (daemon E2E)", () => {
|
||||
|
||||
const offer = decodeOfferFromFragmentUrl(offerUrl) as {
|
||||
endpoints: string[];
|
||||
relay?: { endpoint: string } | null;
|
||||
};
|
||||
|
||||
expect(offer.endpoints).not.toContain("relay.paseo.sh:443");
|
||||
expect(offer.relay).toBe(null);
|
||||
expect(offer.endpoints).toContain(`localhost:${port}`);
|
||||
expect(offer.endpoints).toContain(`192.168.1.12:${port}`);
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Writable } from "node:stream";
|
||||
import net from "node:net";
|
||||
|
||||
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
||||
import { createClientChannel, type Transport } from "@paseo/relay";
|
||||
import { createClientChannel, type Transport } from "@paseo/relay/e2ee";
|
||||
import { createRelayServer } from "@paseo/relay/node";
|
||||
|
||||
function createCapturingLogger() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
createDaemonChannel,
|
||||
type EncryptedChannel,
|
||||
type Transport as RelayTransport,
|
||||
} from "@paseo/relay";
|
||||
} from "@paseo/relay/e2ee";
|
||||
|
||||
type RelayTransportOptions = {
|
||||
logger: pino.Logger;
|
||||
|
||||
17
packages/server/src/shared/connection-offer.ts
Normal file
17
packages/server/src/shared/connection-offer.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ConnectionOfferV1Schema = z.object({
|
||||
v: z.literal(1),
|
||||
sessionId: z.string().min(1),
|
||||
endpoints: z.array(z.string().min(1)).min(1),
|
||||
daemonPublicKeyB64: z.string().min(1),
|
||||
relay: z
|
||||
.object({
|
||||
endpoint: z.string().min(1),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ConnectionOfferV1 = z.infer<typeof ConnectionOfferV1Schema>;
|
||||
|
||||
Reference in New Issue
Block a user