mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(gateway): classify loopback shared-secret clients as local for pairing (#69397)
This commit is contained in:
committed by
Peter Steinberger
parent
43734b1dbd
commit
8ef356d5c3
@@ -322,7 +322,7 @@ describe("handshake auth helpers", () => {
|
||||
).toBe("remote");
|
||||
});
|
||||
|
||||
it("keeps non-CLI clients remote when only the Docker CLI fallback conditions match", () => {
|
||||
it("classifies non-CLI Docker-published loopback clients as shared_secret_loopback_local when auth is token/password", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
|
||||
@@ -340,7 +340,7 @@ describe("handshake auth helpers", () => {
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("remote");
|
||||
).toBe("shared_secret_loopback_local");
|
||||
});
|
||||
|
||||
it("skips backend self-pairing only for direct-local backend clients", () => {
|
||||
@@ -441,4 +441,111 @@ describe("handshake auth helpers", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("classifies non-CLI loopback + shared-secret clients as shared_secret_loopback_local", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_IDS.NODE_HOST,
|
||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||
},
|
||||
} as ConnectParams;
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
connectParams,
|
||||
isLocalClient: false,
|
||||
requestHost: "127.0.0.1:18789",
|
||||
remoteAddress: "127.0.0.1",
|
||||
hasProxyHeaders: false,
|
||||
hasBrowserOriginHeader: false,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("shared_secret_loopback_local");
|
||||
});
|
||||
|
||||
it("keeps non-CLI loopback clients remote without shared-secret auth", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_IDS.NODE_HOST,
|
||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||
},
|
||||
} as ConnectParams;
|
||||
const base = {
|
||||
connectParams,
|
||||
isLocalClient: false,
|
||||
requestHost: "127.0.0.1:18789",
|
||||
remoteAddress: "127.0.0.1",
|
||||
hasProxyHeaders: false,
|
||||
hasBrowserOriginHeader: false,
|
||||
} as const;
|
||||
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
sharedAuthOk: false,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("remote");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "device-token",
|
||||
}),
|
||||
).toBe("remote");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
remoteAddress: "192.168.1.10",
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("remote");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
hasProxyHeaders: true,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("remote");
|
||||
expect(
|
||||
resolvePairingLocality({
|
||||
...base,
|
||||
hasBrowserOriginHeader: true,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe("remote");
|
||||
});
|
||||
|
||||
it("allows silent scope-upgrade for shared_secret_loopback_local", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "shared_secret_loopback_local",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "scope-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "shared_secret_loopback_local",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "role-upgrade",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
locality: "shared_secret_loopback_local",
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export type PairingLocalityKind =
|
||||
| "direct_local"
|
||||
| "cli_container_local"
|
||||
| "browser_container_local"
|
||||
| "shared_secret_loopback_local"
|
||||
| "remote";
|
||||
|
||||
export type HandshakeBrowserSecurityContext = {
|
||||
@@ -111,6 +112,26 @@ function isCliContainerLocalEquivalent(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function isSharedSecretLoopbackLocalEquivalent(params: {
|
||||
requestHost?: string;
|
||||
remoteAddress?: string;
|
||||
hasProxyHeaders: boolean;
|
||||
hasBrowserOriginHeader: boolean;
|
||||
sharedAuthOk: boolean;
|
||||
authMethod: GatewayAuthResult["method"];
|
||||
}): boolean {
|
||||
const usesSharedSecretAuth =
|
||||
params.authMethod === "token" || params.authMethod === "password";
|
||||
return (
|
||||
params.sharedAuthOk &&
|
||||
usesSharedSecretAuth &&
|
||||
!params.hasProxyHeaders &&
|
||||
!params.hasBrowserOriginHeader &&
|
||||
isLoopbackAddress(params.remoteAddress) &&
|
||||
isPrivateOrLoopbackHost(resolveHostName(params.requestHost))
|
||||
);
|
||||
}
|
||||
|
||||
function resolveOriginHost(origin?: string): string {
|
||||
const trimmed = origin?.trim();
|
||||
if (!trimmed) {
|
||||
@@ -190,6 +211,18 @@ export function resolvePairingLocality(params: {
|
||||
) {
|
||||
return "cli_container_local";
|
||||
}
|
||||
if (
|
||||
isSharedSecretLoopbackLocalEquivalent({
|
||||
requestHost: params.requestHost,
|
||||
remoteAddress: params.remoteAddress,
|
||||
hasProxyHeaders: params.hasProxyHeaders,
|
||||
hasBrowserOriginHeader: params.hasBrowserOriginHeader,
|
||||
sharedAuthOk: params.sharedAuthOk,
|
||||
authMethod: params.authMethod,
|
||||
})
|
||||
) {
|
||||
return "shared_secret_loopback_local";
|
||||
}
|
||||
return "remote";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user