mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(onboard): preserve TLS in SSH dashboard hints (#130313)
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
committed by
GitHub
parent
8e9db5e166
commit
0569c6ea6b
@@ -71,14 +71,20 @@ function expectNoLogWith(text: string): void {
|
||||
expect(logMessages().join("\n")).not.toContain(text);
|
||||
}
|
||||
|
||||
function mockSnapshot(token: unknown = "abc") {
|
||||
function mockSnapshot(
|
||||
token: unknown = "abc",
|
||||
gatewayOptions?: {
|
||||
controlUi?: { basePath: string };
|
||||
tls?: { enabled: boolean };
|
||||
},
|
||||
) {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
path: "/tmp/openclaw.json",
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: { gateway: { auth: { token } } },
|
||||
config: { gateway: { auth: { token }, ...gatewayOptions } },
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
});
|
||||
@@ -216,6 +222,30 @@ describe("dashboardCommand", () => {
|
||||
expect(runtime.log).toHaveBeenCalledWith("ssh hint");
|
||||
});
|
||||
|
||||
it("preserves Gateway TLS after remote browser delivery fails", async () => {
|
||||
vi.stubEnv("SSH_CONNECTION", "192.0.2.1 12345 192.0.2.2 22");
|
||||
mockSnapshot("shhhh", {
|
||||
controlUi: { basePath: "/control" },
|
||||
tls: { enabled: true },
|
||||
});
|
||||
resolveControlUiLinksMock.mockReturnValue({
|
||||
httpUrl: "https://127.0.0.1:18789/control/",
|
||||
wsUrl: "wss://127.0.0.1:18789/control",
|
||||
});
|
||||
copyToClipboardMock.mockResolvedValue(false);
|
||||
detectBrowserOpenSupportMock.mockResolvedValue({ ok: true });
|
||||
openUrlMock.mockResolvedValue(false);
|
||||
formatControlUiSshHintMock.mockReturnValue("ssh hint");
|
||||
|
||||
await dashboardCommand(runtime);
|
||||
|
||||
expect(formatControlUiSshHintMock).toHaveBeenCalledWith({
|
||||
port: 18789,
|
||||
basePath: "/control",
|
||||
tlsEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports opener failure without claiming the host has no GUI", async () => {
|
||||
mockSnapshot("shhhh");
|
||||
copyToClipboardMock.mockResolvedValue(true);
|
||||
@@ -243,6 +273,7 @@ describe("dashboardCommand", () => {
|
||||
expect(formatControlUiSshHintMock).toHaveBeenCalledWith({
|
||||
port: 18789,
|
||||
basePath: undefined,
|
||||
tlsEnabled: false,
|
||||
});
|
||||
expect(runtime.log).toHaveBeenCalledWith("ssh hint");
|
||||
});
|
||||
@@ -259,7 +290,11 @@ describe("dashboardCommand", () => {
|
||||
// formatControlUiSshHint must NOT receive the token — the returned
|
||||
// hint string is written to runtime.log, which flows into the same
|
||||
// console-captured log file readable by operator.read-scoped devices.
|
||||
expect(formatControlUiSshHintMock).toHaveBeenCalledWith({ port: 18789, basePath: undefined });
|
||||
expect(formatControlUiSshHintMock).toHaveBeenCalledWith({
|
||||
port: 18789,
|
||||
basePath: undefined,
|
||||
tlsEnabled: false,
|
||||
});
|
||||
const [sshHintOptions] = formatControlUiSshHintMock.mock.calls[0] ?? [];
|
||||
expect(sshHintOptions).not.toHaveProperty("token");
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ export async function dashboardCommand(
|
||||
runtime.log("Run `openclaw doctor`, then retry `openclaw dashboard`.");
|
||||
return;
|
||||
}
|
||||
const { port, basePath, links, includeTokenInUrl } = target;
|
||||
const { port, basePath, links, includeTokenInUrl, tlsConfig } = target;
|
||||
|
||||
runtime.log(`Dashboard URL: ${links.httpUrl}`);
|
||||
runtime.log("One-time browser pairing included in browser/clipboard URL.");
|
||||
@@ -204,6 +204,7 @@ export async function dashboardCommand(
|
||||
hint = formatControlUiSshHint({
|
||||
port,
|
||||
basePath,
|
||||
tlsEnabled: tlsConfig?.enabled === true,
|
||||
});
|
||||
} else {
|
||||
hint = opened
|
||||
@@ -216,6 +217,7 @@ export async function dashboardCommand(
|
||||
hint = formatControlUiSshHint({
|
||||
port,
|
||||
basePath,
|
||||
tlsEnabled: tlsConfig?.enabled === true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -247,6 +247,37 @@ describe("runBrowserHatchHandoff", () => {
|
||||
expect(displayed).not.toContain("#bootstrapToken=");
|
||||
});
|
||||
|
||||
it("prints an HTTPS tunnel destination for a headless loopback TLS Gateway", async () => {
|
||||
const prompter = createWizardPrompter();
|
||||
const config = {
|
||||
gateway: {
|
||||
port: 18789,
|
||||
bind: "loopback" as const,
|
||||
controlUi: { basePath: "/control" },
|
||||
tls: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
await runBrowserHatchHandoff(
|
||||
{ config, prompter },
|
||||
{
|
||||
env: {},
|
||||
platform: "linux",
|
||||
waitForDocument: async () => ({ ready: true }),
|
||||
verifyLoopbackAlias: async () => true,
|
||||
probePresence: async () => ({ reachable: true, clientKeys: [] }),
|
||||
pollForClient: async () => ({ connected: false, reason: "timeout" }),
|
||||
},
|
||||
);
|
||||
|
||||
const displayed = vi
|
||||
.mocked(prompter.note)
|
||||
.mock.calls.map(([message]) => message)
|
||||
.join("\n");
|
||||
expect(displayed).toContain("https://localhost:18789/control/");
|
||||
expect(displayed).not.toContain("http://localhost:18789/control/");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
openerOutcome: "returns false",
|
||||
|
||||
@@ -108,6 +108,7 @@ async function resolveBrowserHatchTarget(
|
||||
sshHint: formatControlUiSshHint({
|
||||
port: shared.port,
|
||||
...(shared.basePath ? { basePath: shared.basePath } : {}),
|
||||
tlsEnabled: shared.tlsConfig?.enabled === true,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
@@ -300,6 +301,7 @@ export async function runBrowserHatchHandoff(
|
||||
...(target.config.gateway?.controlUi?.basePath
|
||||
? { basePath: target.config.gateway.controlUi.basePath }
|
||||
: {}),
|
||||
tlsEnabled: target.tlsConfig?.enabled === true,
|
||||
})
|
||||
: undefined))
|
||||
: undefined;
|
||||
|
||||
@@ -533,8 +533,39 @@ describe("resolveBrowserOpenCommand", () => {
|
||||
});
|
||||
|
||||
describe("formatControlUiSshHint", () => {
|
||||
it.each([
|
||||
{
|
||||
label: "plain HTTP root",
|
||||
tlsEnabled: false,
|
||||
basePath: undefined,
|
||||
expectedUrl: "http://localhost:18789/",
|
||||
},
|
||||
{
|
||||
label: "plain HTTP base path",
|
||||
tlsEnabled: false,
|
||||
basePath: "/control",
|
||||
expectedUrl: "http://localhost:18789/control/",
|
||||
},
|
||||
{
|
||||
label: "HTTPS root",
|
||||
tlsEnabled: true,
|
||||
basePath: undefined,
|
||||
expectedUrl: "https://localhost:18789/",
|
||||
},
|
||||
{
|
||||
label: "HTTPS base path",
|
||||
tlsEnabled: true,
|
||||
basePath: "/control",
|
||||
expectedUrl: "https://localhost:18789/control/",
|
||||
},
|
||||
])("uses the Gateway transport for $label", ({ tlsEnabled, basePath, expectedUrl }) => {
|
||||
const hint = formatControlUiSshHint({ port: 18789, basePath, tlsEnabled });
|
||||
|
||||
expect(hint).toContain(`Then open:\n${expectedUrl}`);
|
||||
});
|
||||
|
||||
it("includes the IPv4-only BYOH note and workaround", () => {
|
||||
const hint = formatControlUiSshHint({ port: 18789 });
|
||||
const hint = formatControlUiSshHint({ port: 18789, tlsEnabled: false });
|
||||
expect(hint).toContain("BYOH note: lan, tailnet, and custom bind are currently IPv4-only.");
|
||||
expect(hint).toContain(
|
||||
"If your host is IPv6-only, use an IPv4 sidecar or proxy in front of the Gateway.",
|
||||
|
||||
@@ -187,10 +187,15 @@ export function applyWizardMetadata(
|
||||
}
|
||||
|
||||
/** Formats the no-GUI SSH tunnel hint for opening the Control UI remotely. */
|
||||
export function formatControlUiSshHint(params: { port: number; basePath?: string }): string {
|
||||
export function formatControlUiSshHint(params: {
|
||||
port: number;
|
||||
basePath?: string;
|
||||
tlsEnabled: boolean;
|
||||
}): string {
|
||||
const basePath = normalizeControlUiBasePath(params.basePath);
|
||||
const uiPath = basePath ? `${basePath}/` : "/";
|
||||
const localUrl = `http://localhost:${params.port}${uiPath}`;
|
||||
const protocol = params.tlsEnabled ? "https" : "http";
|
||||
const localUrl = `${protocol}://localhost:${params.port}${uiPath}`;
|
||||
const sshTarget = resolveSshTargetHint();
|
||||
return [
|
||||
"No GUI detected. Open from your computer:",
|
||||
|
||||
Reference in New Issue
Block a user