mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-18 12:08:52 +00:00
Validate Control UI loopback retry endpoints [AI] (#80900)
* fix: validate loopback retry endpoints * fix: validate loopback retry endpoints * docs: add changelog entry for PR merge
This commit is contained in:
committed by
GitHub
parent
fd12a48ee1
commit
5ef8d1ab5e
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Validate Control UI loopback retry endpoints [AI]. (#80900) Thanks @pgondhi987.
|
||||
- Harden exported markdown link rendering [AI]. (#80902) Thanks @pgondhi987.
|
||||
- fix(gateway): honor minimal discovery mode for wide-area DNS-SD [AI]. (#80903) Thanks @pgondhi987.
|
||||
- slack: enforce reaction notification policy [AI]. (#80907) Thanks @pgondhi987.
|
||||
|
||||
@@ -623,6 +623,34 @@ describe("GatewayBrowserClient", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("reconnects without cached device token for DNS hosts beginning with a 127 label", async () => {
|
||||
useNodeFakeTimers();
|
||||
const client = new GatewayBrowserClient({
|
||||
url: "ws://127.example.invalid:18789",
|
||||
token: "shared-auth-token",
|
||||
});
|
||||
|
||||
try {
|
||||
const { ws: firstWs, connectFrame: firstConnect } = await startConnect(client);
|
||||
expect(firstConnect.params?.auth?.token).toBe("shared-auth-token");
|
||||
expect(firstConnect.params?.auth?.deviceToken).toBeUndefined();
|
||||
|
||||
emitRetryableTokenMismatch(firstWs, firstConnect.id);
|
||||
await expectSocketClosed(firstWs);
|
||||
firstWs.emitClose(4008, "connect failed");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(800);
|
||||
const secondWs = getLatestWebSocket();
|
||||
expect(secondWs).not.toBe(firstWs);
|
||||
const { connectFrame: secondConnect } = await continueConnect(secondWs, "nonce-2");
|
||||
expect(secondConnect.params?.auth?.token).toBe("shared-auth-token");
|
||||
expect(secondConnect.params?.auth?.deviceToken).toBeUndefined();
|
||||
} finally {
|
||||
client.stop();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("retries startup-unavailable connect responses without terminal callbacks", async () => {
|
||||
useNodeFakeTimers();
|
||||
const onClose = vi.fn();
|
||||
@@ -910,6 +938,44 @@ describe("shouldRetryWithDeviceToken", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("allows a bounded retry for loopback IPv4 addresses in the 127 block", () => {
|
||||
expect(
|
||||
shouldRetryWithDeviceToken({
|
||||
deviceTokenRetryBudgetUsed: false,
|
||||
authDeviceToken: undefined,
|
||||
explicitGatewayToken: "shared-auth-token",
|
||||
deviceIdentity: {
|
||||
deviceId: "device-1",
|
||||
privateKey: "private-key", // pragma: allowlist secret
|
||||
publicKey: "public-key", // pragma: allowlist secret
|
||||
},
|
||||
storedToken: "stored-device-token",
|
||||
canRetryWithDeviceTokenHint: true,
|
||||
url: "ws://127.255.10.42:18789",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks the retry for DNS hosts beginning with a 127 label", () => {
|
||||
for (const url of ["ws://127.example.invalid:18789", "ws://127.0.0.1.example.invalid:18789"]) {
|
||||
expect(
|
||||
shouldRetryWithDeviceToken({
|
||||
deviceTokenRetryBudgetUsed: false,
|
||||
authDeviceToken: undefined,
|
||||
explicitGatewayToken: "shared-auth-token",
|
||||
deviceIdentity: {
|
||||
deviceId: "device-1",
|
||||
privateKey: "private-key", // pragma: allowlist secret
|
||||
publicKey: "public-key", // pragma: allowlist secret
|
||||
},
|
||||
storedToken: "stored-device-token",
|
||||
canRetryWithDeviceTokenHint: true,
|
||||
url,
|
||||
}),
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("blocks the retry after the one-shot budget is spent", () => {
|
||||
expect(
|
||||
shouldRetryWithDeviceToken({
|
||||
|
||||
@@ -98,13 +98,26 @@ export function isNonRecoverableAuthError(error: GatewayErrorInfo | undefined):
|
||||
);
|
||||
}
|
||||
|
||||
function isLoopbackIPv4Host(host: string): boolean {
|
||||
const octets = host.split(".");
|
||||
if (octets.length !== 4 || octets[0] !== "127") {
|
||||
return false;
|
||||
}
|
||||
return octets.every((octet) => {
|
||||
if (!/^\d+$/.test(octet)) {
|
||||
return false;
|
||||
}
|
||||
const value = Number(octet);
|
||||
return value >= 0 && value <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
function isTrustedRetryEndpoint(url: string): boolean {
|
||||
try {
|
||||
const gatewayUrl = new URL(url, window.location.href);
|
||||
const host = gatewayUrl.hostname.trim().toLowerCase();
|
||||
const isLoopbackHost =
|
||||
host === "localhost" || host === "::1" || host === "[::1]" || host === "127.0.0.1";
|
||||
const isLoopbackIPv4 = host.startsWith("127.");
|
||||
const isLoopbackHost = host === "localhost" || host === "::1" || host === "[::1]";
|
||||
const isLoopbackIPv4 = isLoopbackIPv4Host(host);
|
||||
if (isLoopbackHost || isLoopbackIPv4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user