mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
feat(protocol): expose structured host connection targets
This commit is contained in:
@@ -1,5 +1,33 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { parseHostTarget } from "./host-target.js";
|
||||
import { serializeSshTransportUri } from "./ssh-transport.js";
|
||||
import { serializeSocketHostTarget } from "./host-target.js";
|
||||
|
||||
test("SSH and local socket targets round trip through their serializers", () => {
|
||||
const ssh = parseHostTarget("ssh://deploy@build-box:2222?daemonPort=7777");
|
||||
if (ssh.kind !== "ssh") throw new Error("expected SSH");
|
||||
expect(parseHostTarget(serializeSshTransportUri(ssh.ssh))).toEqual(ssh);
|
||||
for (const [type, path] of [
|
||||
["unix", "/tmp/paseo.sock"],
|
||||
["pipe", "\\\\.\\pipe\\paseo"],
|
||||
] as const) {
|
||||
const target = parseHostTarget(`${type}://${path}`);
|
||||
expect(parseHostTarget(serializeSocketHostTarget(type, path))).toEqual(target);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
["example.com:6767", { type: "directTcp", endpoint: "example.com:6767", useTls: false }],
|
||||
[
|
||||
"tcp://example.com:6767?ssl=true",
|
||||
{ type: "directTcp", endpoint: "example.com:6767", useTls: true },
|
||||
],
|
||||
["wss://example.com/ws", { type: "directTcp", endpoint: "example.com:443", useTls: true }],
|
||||
["unix:///tmp/paseo.sock", { type: "directSocket", path: "/tmp/paseo.sock" }],
|
||||
["pipe://mypipe", { type: "directPipe", path: "mypipe" }],
|
||||
])("%s exposes its structured connection", (input, connection) => {
|
||||
expect(parseHostTarget(input)).toMatchObject({ kind: "socket", connection });
|
||||
});
|
||||
|
||||
test.each([
|
||||
["127.0.0.1:6767", "tcp://127.0.0.1:6767"],
|
||||
|
||||
@@ -14,12 +14,23 @@ export type HostTarget =
|
||||
| {
|
||||
kind: "socket";
|
||||
url: string;
|
||||
connection:
|
||||
| { type: "directTcp"; endpoint: string; useTls: boolean }
|
||||
| { type: "directSocket"; path: string }
|
||||
| { type: "directPipe"; path: string }
|
||||
| { type: "relay"; offer: ConnectionOffer };
|
||||
socketPath?: string;
|
||||
password?: string;
|
||||
offer?: ConnectionOffer;
|
||||
description: string;
|
||||
};
|
||||
|
||||
/** Serialize a local socket target in the shared host grammar. */
|
||||
export function serializeSocketHostTarget(type: "unix" | "pipe", path: string): string {
|
||||
if (!path || path === "~") throw new Error("Invalid socket path");
|
||||
return `${type}://${path}`;
|
||||
}
|
||||
|
||||
/** The shared saved-host and CLI grammar. Errors never echo credential-bearing input. */
|
||||
export function parseHostTarget(input: string): HostTarget {
|
||||
try {
|
||||
@@ -57,6 +68,9 @@ function parse(input: string): HostTarget {
|
||||
const pipe = input.startsWith("pipe://") || input.startsWith("\\\\.\\pipe\\");
|
||||
return {
|
||||
kind: "socket",
|
||||
connection: pipe
|
||||
? { type: "directPipe", path: socketPath }
|
||||
: { type: "directSocket", path: socketPath },
|
||||
socketPath,
|
||||
url: pipe ? "ws://localhost/ws" : `ws+unix://${socketPath}:/ws`,
|
||||
description: `${pipe ? "pipe" : "unix"}://${socketPath}`,
|
||||
@@ -67,6 +81,7 @@ function parse(input: string): HostTarget {
|
||||
const endpoint = validatedEndpoint(/^\d+$/.test(input) ? `127.0.0.1:${input}` : input);
|
||||
return {
|
||||
kind: "socket",
|
||||
connection: { type: "directTcp", endpoint, useTls: false },
|
||||
url: buildDaemonWebSocketUrl(endpoint, { useTls: false }),
|
||||
description: `tcp://${endpoint}`,
|
||||
};
|
||||
@@ -75,6 +90,7 @@ function relayTarget(offer: ConnectionOffer, password?: string): HostTarget {
|
||||
const endpoint = validatedEndpoint(offer.relay.endpoint);
|
||||
return {
|
||||
kind: "socket",
|
||||
connection: { type: "relay", offer },
|
||||
url: buildRelayWebSocketUrl({
|
||||
endpoint,
|
||||
serverId: offer.serverId,
|
||||
@@ -97,6 +113,7 @@ function tcpTarget(input: string): HostTarget {
|
||||
);
|
||||
return {
|
||||
kind: "socket",
|
||||
connection: { type: "directTcp", endpoint, useTls: parts.useTls },
|
||||
url: buildDaemonWebSocketUrl(endpoint, { useTls: parts.useTls }),
|
||||
password: parts.password,
|
||||
description: `tcp://${endpoint}`,
|
||||
@@ -109,7 +126,15 @@ function websocketTarget(input: string): HostTarget {
|
||||
// Unknown query parameters may themselves contain credentials; discovery shows only the origin.
|
||||
if (url.pathname === "/") url.pathname = "/ws";
|
||||
url.searchParams.delete("password");
|
||||
return { kind: "socket", url: url.toString(), password, description: url.origin };
|
||||
const useTls = url.protocol === "wss:";
|
||||
const endpoint = `${url.hostname}:${url.port || (useTls ? "443" : "80")}`;
|
||||
return {
|
||||
kind: "socket",
|
||||
connection: { type: "directTcp", endpoint, useTls },
|
||||
url: url.toString(),
|
||||
password,
|
||||
description: url.origin,
|
||||
};
|
||||
}
|
||||
|
||||
function sshTarget(input: string): HostTarget {
|
||||
|
||||
@@ -60,6 +60,19 @@ export function parseSshTransportUri(value: string): SshTransportTarget {
|
||||
};
|
||||
}
|
||||
|
||||
/** Serialize an SSH target without daemon credentials. */
|
||||
export function serializeSshTransportUri(target: SshTransportTarget): string {
|
||||
const host = validateSshHost(target.host);
|
||||
const separator = host.lastIndexOf("@");
|
||||
const username = separator < 0 ? "" : `${encodeURIComponent(host.slice(0, separator))}@`;
|
||||
const hostname = host.slice(separator + 1);
|
||||
const authority = hostname.includes(":") ? `[${hostname}]` : hostname;
|
||||
const port = target.sshPort === undefined ? "" : `:${validatePort(target.sshPort, "SSH port")}`;
|
||||
const daemonPort = validatePort(target.daemonPort, "Daemon port");
|
||||
const query = daemonPort === DEFAULT_SSH_DAEMON_PORT ? "" : `?daemonPort=${daemonPort}`;
|
||||
return `ssh://${username}${authority}${port}${query}`;
|
||||
}
|
||||
|
||||
export function buildSshTunnelArgs(target: SshTransportTarget): string[] {
|
||||
const host = validateSshHost(target.host);
|
||||
const daemonPort = validatePort(target.daemonPort, "Daemon port");
|
||||
|
||||
Reference in New Issue
Block a user