mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(device-pair): reject invalid remote setup URLs
Fail setup-code generation when gateway.remote.url is configured but malformed, instead of falling back to a bind-derived URL and issuing a bootstrap token.
This commit is contained in:
@@ -70,6 +70,7 @@ type ApprovedPairingDevice = ApprovedPairingResult["device"];
|
||||
const INTERNAL_PAIRING_SCOPES = ["operator.write", "operator.pairing"];
|
||||
|
||||
function createApi(params?: {
|
||||
config?: OpenClawPluginApi["config"];
|
||||
runtime?: OpenClawPluginApi["runtime"];
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
registerCommand?: (command: OpenClawPluginCommandDefinition) => void;
|
||||
@@ -78,7 +79,7 @@ function createApi(params?: {
|
||||
id: "device-pair",
|
||||
name: "device-pair",
|
||||
source: "test",
|
||||
config: {
|
||||
config: params?.config ?? {
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
@@ -96,6 +97,7 @@ function createApi(params?: {
|
||||
}
|
||||
|
||||
function registerPairCommand(params?: {
|
||||
config?: OpenClawPluginApi["config"];
|
||||
runtime?: OpenClawPluginApi["runtime"];
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
}): OpenClawPluginCommandDefinition {
|
||||
@@ -649,6 +651,36 @@ describe("device-pair /pair default setup code", () => {
|
||||
expect(result).toEqual({ text: "Error: Configured publicUrl is invalid." });
|
||||
});
|
||||
|
||||
it("rejects invalid gateway.remote.url before falling back to bind-derived setup urls", async () => {
|
||||
const command = registerPairCommand({
|
||||
config: {
|
||||
gateway: {
|
||||
bind: "custom",
|
||||
customBindHost: "127.0.0.1",
|
||||
remote: { url: "http://localhost:notaport" },
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "gateway-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginConfig: {
|
||||
publicUrl: undefined,
|
||||
},
|
||||
});
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "",
|
||||
commandBody: "/pair",
|
||||
gatewayClientScopes: ["operator.write", "operator.pairing"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ text: "Error: Configured gateway.remote.url is invalid." });
|
||||
});
|
||||
|
||||
it.each([
|
||||
"http://localhost:notaport",
|
||||
"http:gateway.example.test",
|
||||
|
||||
@@ -321,6 +321,12 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
|
||||
return { error: "Configured publicUrl is invalid." };
|
||||
}
|
||||
|
||||
const configuredRemoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url);
|
||||
const remoteUrl = configuredRemoteUrl ? normalizeUrl(configuredRemoteUrl, scheme) : null;
|
||||
if (configuredRemoteUrl && !remoteUrl) {
|
||||
return { error: "Configured gateway.remote.url is invalid." };
|
||||
}
|
||||
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
if (tailscaleMode === "serve" || tailscaleMode === "funnel") {
|
||||
const host = await resolveTailnetHost();
|
||||
@@ -330,12 +336,8 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
|
||||
return { url: `wss://${host}`, source: `gateway.tailscale.mode=${tailscaleMode}` };
|
||||
}
|
||||
|
||||
const remoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url);
|
||||
if (remoteUrl) {
|
||||
const url = normalizeUrl(remoteUrl, scheme);
|
||||
if (url) {
|
||||
return { url, source: "gateway.remote.url" };
|
||||
}
|
||||
return { url: remoteUrl, source: "gateway.remote.url" };
|
||||
}
|
||||
|
||||
const bindResult = resolveGatewayBindUrl({
|
||||
|
||||
@@ -413,6 +413,23 @@ describe("registerQrCli", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid gateway.remote.url before printing remote setup codes", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
bind: "custom",
|
||||
customBindHost: "127.0.0.1",
|
||||
remote: { url: "http://localhost:notaport", token: "remote-tok" },
|
||||
auth: { mode: "token", token: "local-tok" },
|
||||
},
|
||||
});
|
||||
|
||||
await expectQrExit(["--setup-code-only", "--remote"]);
|
||||
|
||||
const output = runtimeError.mock.calls.map((call) => readRuntimeCallText(call)).join("\n");
|
||||
expect(output).toContain("Configured gateway.remote.url is invalid.");
|
||||
expect(runtime.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs remote secret diagnostics in non-json output mode", async () => {
|
||||
loadConfig.mockReturnValue(createRemoteQrConfig());
|
||||
resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({
|
||||
|
||||
@@ -222,6 +222,24 @@ describe("pairing setup code", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid gateway.remote.url before falling back to bind-derived setup urls", async () => {
|
||||
await expectResolvedSetupFailureCase({
|
||||
config: {
|
||||
gateway: {
|
||||
bind: "custom",
|
||||
customBindHost: "127.0.0.1",
|
||||
remote: { url: "http://localhost:notaport" },
|
||||
auth: { mode: "token", token: "tok_123" },
|
||||
},
|
||||
},
|
||||
options: {
|
||||
preferRemoteUrl: true,
|
||||
},
|
||||
expectedError: "Configured gateway.remote.url is invalid.",
|
||||
});
|
||||
expect(issueDeviceBootstrapTokenMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"localhost:notaport",
|
||||
"http://localhost:notaport",
|
||||
|
||||
@@ -285,10 +285,11 @@ async function resolveGatewayUrl(
|
||||
}
|
||||
|
||||
const remoteUrlRaw = cfg.gateway?.remote?.url;
|
||||
const remoteUrl =
|
||||
typeof remoteUrlRaw === "string" && remoteUrlRaw.trim()
|
||||
? normalizeUrl(remoteUrlRaw, scheme)
|
||||
: null;
|
||||
const hasRemoteUrl = typeof remoteUrlRaw === "string" && remoteUrlRaw.trim();
|
||||
const remoteUrl = hasRemoteUrl ? normalizeUrl(remoteUrlRaw, scheme) : null;
|
||||
if (hasRemoteUrl && !remoteUrl) {
|
||||
return { error: "Configured gateway.remote.url is invalid." };
|
||||
}
|
||||
if (opts.preferRemoteUrl && remoteUrl) {
|
||||
return { url: remoteUrl, source: "gateway.remote.url" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user