mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 06:02:13 +00:00
Gateway: allow Docker loopback Control UI pairing
This commit is contained in:
@@ -107,6 +107,15 @@ describe("handshake auth helpers", () => {
|
||||
reason: "scope-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "browser_container_local",
|
||||
hasBrowserOriginHeader: true,
|
||||
isControlUi: true,
|
||||
isWebchat: true,
|
||||
reason: "not-paired",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "direct_local",
|
||||
@@ -150,6 +159,112 @@ describe("handshake auth helpers", () => {
|
||||
).toBe("direct_local");
|
||||
});
|
||||
|
||||
it("classifies Docker-published loopback Control UI as browser-container-local", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_IDS.CONTROL_UI,
|
||||
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
||||
},
|
||||
} as ConnectParams;
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
connectParams,
|
||||
isLocalClient: false,
|
||||
requestHost: "127.0.0.1:18789",
|
||||
requestOrigin: "http://127.0.0.1:18789",
|
||||
remoteAddress: "172.17.0.1",
|
||||
hasProxyHeaders: false,
|
||||
hasBrowserOriginHeader: true,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("browser_container_local");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
connectParams,
|
||||
isLocalClient: false,
|
||||
requestHost: "localhost:18789",
|
||||
requestOrigin: "http://localhost:18789",
|
||||
remoteAddress: "172.17.0.1",
|
||||
hasProxyHeaders: false,
|
||||
hasBrowserOriginHeader: true,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "password",
|
||||
}),
|
||||
).toBe("browser_container_local");
|
||||
});
|
||||
|
||||
it("keeps Docker-published non-loopback Control UI origins remote", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_IDS.CONTROL_UI,
|
||||
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
||||
},
|
||||
} as ConnectParams;
|
||||
const base = {
|
||||
connectParams,
|
||||
isLocalClient: false,
|
||||
remoteAddress: "172.17.0.1",
|
||||
hasProxyHeaders: false,
|
||||
hasBrowserOriginHeader: true,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token" as const,
|
||||
};
|
||||
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
requestHost: "192.168.1.10:18789",
|
||||
requestOrigin: "http://192.168.1.10:18789",
|
||||
}),
|
||||
).toBe("remote");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
requestHost: "127.0.0.1:18789",
|
||||
requestOrigin: "https://app.example",
|
||||
}),
|
||||
).toBe("remote");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
requestHost: "127.0.0.1:18789",
|
||||
requestOrigin: "http://127.0.0.1:18789",
|
||||
hasProxyHeaders: true,
|
||||
}),
|
||||
).toBe("remote");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
requestHost: "127.0.0.1:18789",
|
||||
requestOrigin: "http://127.0.0.1:18789",
|
||||
sharedAuthOk: false,
|
||||
}),
|
||||
).toBe("remote");
|
||||
});
|
||||
|
||||
it("keeps non-Control-UI clients remote for browser-container-local conditions", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
},
|
||||
} as ConnectParams;
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
connectParams,
|
||||
isLocalClient: false,
|
||||
requestHost: "127.0.0.1:18789",
|
||||
requestOrigin: "http://127.0.0.1:18789",
|
||||
remoteAddress: "172.17.0.1",
|
||||
hasProxyHeaders: false,
|
||||
hasBrowserOriginHeader: true,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("remote");
|
||||
});
|
||||
|
||||
it("classifies CLI loopback/private-host connects as cli_container_local only with shared auth", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
|
||||
@@ -2,14 +2,24 @@ import { verifyDeviceSignature } from "../../../infra/device-identity.js";
|
||||
import type { AuthRateLimiter } from "../../auth-rate-limit.js";
|
||||
import type { GatewayAuthResult } from "../../auth.js";
|
||||
import { buildDeviceAuthPayload, buildDeviceAuthPayloadV3 } from "../../device-auth.js";
|
||||
import { isLoopbackAddress, isPrivateOrLoopbackHost, resolveHostName } from "../../net.js";
|
||||
import {
|
||||
isLoopbackAddress,
|
||||
isLoopbackHost,
|
||||
isPrivateOrLoopbackAddress,
|
||||
isPrivateOrLoopbackHost,
|
||||
resolveHostName,
|
||||
} from "../../net.js";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js";
|
||||
import type { ConnectParams } from "../../protocol/index.js";
|
||||
import type { AuthProvidedKind } from "./auth-messages.js";
|
||||
|
||||
export const BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP = "198.18.0.1";
|
||||
export const BROWSER_ORIGIN_RATE_LIMIT_KEY_PREFIX = "browser-origin:";
|
||||
export type PairingLocalityKind = "direct_local" | "cli_container_local" | "remote";
|
||||
export type PairingLocalityKind =
|
||||
| "direct_local"
|
||||
| "cli_container_local"
|
||||
| "browser_container_local"
|
||||
| "remote";
|
||||
|
||||
export type HandshakeBrowserSecurityContext = {
|
||||
hasBrowserOriginHeader: boolean;
|
||||
@@ -100,10 +110,49 @@ function isCliContainerLocalEquivalent(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveOriginHost(origin?: string): string {
|
||||
const trimmed = origin?.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return new URL(trimmed).hostname;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function isControlUiBrowserContainerLocalEquivalent(params: {
|
||||
connectParams: ConnectParams;
|
||||
requestHost?: string;
|
||||
requestOrigin?: string;
|
||||
remoteAddress?: string;
|
||||
hasProxyHeaders: boolean;
|
||||
hasBrowserOriginHeader: boolean;
|
||||
sharedAuthOk: boolean;
|
||||
authMethod: GatewayAuthResult["method"];
|
||||
}): boolean {
|
||||
const isControlUiBrowser =
|
||||
params.connectParams.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI &&
|
||||
params.connectParams.client.mode === GATEWAY_CLIENT_MODES.WEBCHAT;
|
||||
const usesSharedSecretAuth = params.authMethod === "token" || params.authMethod === "password";
|
||||
return (
|
||||
isControlUiBrowser &&
|
||||
params.sharedAuthOk &&
|
||||
usesSharedSecretAuth &&
|
||||
!params.hasProxyHeaders &&
|
||||
params.hasBrowserOriginHeader &&
|
||||
isPrivateOrLoopbackAddress(params.remoteAddress) &&
|
||||
isLoopbackHost(resolveHostName(params.requestHost)) &&
|
||||
isLoopbackHost(resolveOriginHost(params.requestOrigin))
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePairingLocality(params: {
|
||||
connectParams: ConnectParams;
|
||||
isLocalClient: boolean;
|
||||
requestHost?: string;
|
||||
requestOrigin?: string;
|
||||
remoteAddress?: string;
|
||||
hasProxyHeaders: boolean;
|
||||
hasBrowserOriginHeader: boolean;
|
||||
@@ -113,6 +162,20 @@ export function resolvePairingLocality(params: {
|
||||
if (params.isLocalClient) {
|
||||
return "direct_local";
|
||||
}
|
||||
if (
|
||||
isControlUiBrowserContainerLocalEquivalent({
|
||||
connectParams: params.connectParams,
|
||||
requestHost: params.requestHost,
|
||||
requestOrigin: params.requestOrigin,
|
||||
remoteAddress: params.remoteAddress,
|
||||
hasProxyHeaders: params.hasProxyHeaders,
|
||||
hasBrowserOriginHeader: params.hasBrowserOriginHeader,
|
||||
sharedAuthOk: params.sharedAuthOk,
|
||||
authMethod: params.authMethod,
|
||||
})
|
||||
) {
|
||||
return "browser_container_local";
|
||||
}
|
||||
if (
|
||||
isCliContainerLocalEquivalent({
|
||||
connectParams: params.connectParams,
|
||||
|
||||
@@ -737,6 +737,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
connectParams,
|
||||
isLocalClient,
|
||||
requestHost,
|
||||
requestOrigin,
|
||||
remoteAddress: remoteAddr,
|
||||
hasProxyHeaders,
|
||||
hasBrowserOriginHeader,
|
||||
|
||||
Reference in New Issue
Block a user