Files
openclaw/apps/shared/OpenClawKit/Tests/OpenClawKitTests/DeepLinksSecurityTests.swift
Peter Steinberger d44f70eb4b feat(pairing): one-paste device pairing via oc-pair setup links (#120768)
* feat(pairing): one-paste device pairing via oc-pair setup links

Implements milestone 3 from docs/plan/runners.md.

* fix(pairing): sign bootstrap handshake, keep URL candidates, wire pairing countdown

* test(gateway): update client callsite guard

* fix(pairing): preserve setup URL context paths

* fix(ui): keep pairing help aligned with setup mode

* fix(pairing): isolate bootstrap credentials

* perf(ui): keep one-paste pairing within bundle budget

* refactor(pairing): isolate native pair URL prefix parsing

* fix(pairing): preserve candidate lifecycle state

* fix(pairing): retire shared credentials after bootstrap

* fix(pairing): apply rotated manifest through client owner

* test(pairing): prove bootstrap retirement across reconnect

* fix(pairing): preserve native gateway context paths

* fix(pairing): carry native context paths through reconnect

* fix(ios): preserve encoded gateway context path

* chore(plugin-sdk): refresh pairing API baselines
2026-08-12 02:01:33 -07:00

331 lines
13 KiB
Swift

import Foundation
import OpenClawKit
import OpenClawProtocol
import Testing
private func setupCode(from payload: String) -> String {
Data(payload.utf8)
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
private func gatewayLink(from raw: String) -> GatewayConnectDeepLink? {
guard let url = URL(string: raw),
case let .gateway(link)? = DeepLinkParser.parse(url)
else { return nil }
return link
}
@Suite struct DeepLinksSecurityTests {
@Test func setupResultInitializerKeepsLegacySignature() {
let result = DevicePairSetupCodeResult(
setupcode: "code",
qrdataurl: nil,
gatewayurl: "wss://gateway.example.com",
auth: AnyCodable("token"),
urlsource: "config")
#expect(result.gatewayurls == nil)
}
@Test func dashboardDeepLinkParses() {
let url = URL(string: "openclaw://dashboard")!
#expect(DeepLinkParser.parse(url) == .dashboard)
}
@Test func debugDashboardDeepLinkParses() {
let url = URL(string: "openclaw-debug://dashboard")!
#expect(DeepLinkParser.parse(url) == .dashboard)
}
@Test func gatewayDeepLinkUsesTlsDefaultPortWhenPortMissing() {
let link = gatewayLink(from: "openclaw://gateway?host=gateway.example.com&tls=1")
#expect(link?.port == 443)
#expect(link?.tls == true)
}
@Test func gatewayDeepLinkUsesPlaintextDefaultPortWhenPortMissing() {
let link = gatewayLink(from: "openclaw://gateway?host=127.0.0.1&tls=0")
#expect(link?.port == 18789)
#expect(link?.tls == false)
}
@Test func gatewayDeepLinkPreservesExplicitTlsPort() {
let link = gatewayLink(from: "openclaw://gateway?host=gateway.example.com&port=18789&tls=1")
#expect(link?.port == 18789)
#expect(link?.tls == true)
}
@Test func gatewayDeepLinkRejectsInsecureNonLoopbackWs() {
let url = URL(
string: "openclaw://gateway?host=attacker.example&port=18789&tls=0&token=abc")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func gatewayDeepLinkRejectsInsecurePrefixBypassHost() {
let url = URL(
string: "openclaw://gateway?host=127.attacker.example&port=18789&tls=0&token=abc")!
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func gatewayDeepLinkAllowsLoopbackWs() {
let url = URL(
string: "openclaw://gateway?host=127.0.0.1&port=18789&tls=0&token=abc")!
#expect(
DeepLinkParser.parse(url) == .gateway(
.init(
host: "127.0.0.1",
port: 18789,
tls: false,
bootstrapToken: nil,
token: "abc",
password: nil)))
}
@Test func setupCodeRejectsInsecureNonLoopbackWs() {
let payload = #"{"url":"ws://attacker.example:18789","bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeRejectsInsecurePrefixBypassHost() {
let payload = #"{"url":"ws://127.attacker.example:18789","bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeAllowsLoopbackWs() {
let payload = #"{"url":"ws://127.0.0.1:18789","bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "127.0.0.1",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeAcceptsPairingURLWrapperWithoutLowercasingPayload() {
let payload = #"{"url":"wss://gateway.example:8443","bootstrapToken":"Bootstrap-AbC123"}"#
let code = setupCode(from: payload)
#expect(
GatewayConnectDeepLink.fromSetupCode("oc-pair://\(code)") ==
GatewayConnectDeepLink.fromSetupCode(code))
}
@Test func setupCodePreservesPrimaryGatewayContextPath() {
let payload = #"{"url":"wss://gateway.example/openclaw-gw","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link?.contextPath == "/openclaw-gw")
#expect(link?.websocketURL?.absoluteString == "wss://gateway.example:443/openclaw-gw")
}
@Test func setupCodeDecodesGatewayContextPathExactlyOnce() {
let payload = #"{"url":"wss://gateway.example/openclaw%20gateway","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link?.contextPath == "/openclaw%20gateway")
#expect(link?.websocketURL?.absoluteString == "wss://gateway.example:443/openclaw%20gateway")
}
@Test func setupCodePreservesEscapedGatewayPathDelimiter() {
let payload = #"{"url":"wss://gateway.example/openclaw%2Fgateway","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link?.contextPath == "/openclaw%2Fgateway")
#expect(link?.websocketURL?.absoluteString == "wss://gateway.example:443/openclaw%2Fgateway")
}
@Test func setupCodePreservesNonUTF8GatewayPathOctet() {
let payload = #"{"url":"wss://gateway.example/openclaw%FFgateway","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link?.contextPath == "/openclaw%FFgateway")
#expect(link?.websocketURL?.absoluteString == "wss://gateway.example:443/openclaw%FFgateway")
}
@Test func setupCodeAllowsPrivateLanWs() {
let payload = #"{"url":"ws://192.168.1.20:18789","bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "192.168.1.20",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeAllowsMDNSWs() {
let payload = #"{"url":"ws://openclaw.local:18789","bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeParsesOrderedGatewayFallbacks() throws {
let payload = #"{"url":"ws://192.168.1.20:18789/lan-gw","urls":["ws://192.168.1.20:18789/lan-gw","wss://gateway.tailnet.ts.net:8443/tailnet-gw"],"bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link?.connectionEndpoints == [
.init(host: "192.168.1.20", port: 18789, tls: false, contextPath: "/lan-gw"),
.init(host: "gateway.tailnet.ts.net", port: 8443, tls: true, contextPath: "/tailnet-gw"),
])
#expect(try link?.selectingEndpoint(#require(link?.connectionEndpoints[1])) == .init(
host: "gateway.tailnet.ts.net",
port: 8443,
tls: true,
contextPath: "/tailnet-gw",
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func legacyEncodedGatewayLinkDecodesWithoutFallbacks() throws {
let payload = #"{"host":"gateway.tailnet.ts.net","port":443,"tls":true}"#
let link = try JSONDecoder().decode(
GatewayConnectDeepLink.self,
from: Data(payload.utf8))
#expect(link.contextPath == nil)
#expect(link.fallbackEndpoints.isEmpty)
}
@Test func legacyEncodedFallbackEndpointDecodesWithoutContextPath() throws {
let payload = #"{"host":"gateway.example","port":443,"tls":true,"fallbackEndpoints":[{"host":"fallback.example","port":443,"tls":true}]}"#
let link = try JSONDecoder().decode(
GatewayConnectDeepLink.self,
from: Data(payload.utf8))
#expect(link.fallbackEndpoints == [
.init(host: "fallback.example", port: 443, tls: true),
])
}
@Test func setupCodeRejectsGatewayURLMetadata() {
let urls = [
"wss://user@gateway.example/openclaw-gw",
"wss://gateway.example/openclaw-gw?mode=setup",
"wss://gateway.example/openclaw-gw#fragment",
]
for url in urls {
let payload = #"{"url":"\#(url)","bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
}
@Test func setupCodeDropsInsecureGatewayFallbacks() {
let payload = #"{"url":"ws://attacker.example:18789","urls":["ws://attacker.example:18789","wss://gateway.tailnet.ts.net"],"bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "gateway.tailnet.ts.net",
port: 443,
tls: true,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeCapsGatewayEndpoints() throws {
let urls = (0..<10).map { "wss://gateway-\($0).example.com" }
let data = try JSONSerialization.data(withJSONObject: ["url": urls[0], "urls": urls])
let payload = try #require(String(data: data, encoding: .utf8))
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link?.connectionEndpoints.count == 8)
#expect(link?.connectionEndpoints.last?.host == "gateway-7.example.com")
}
@Test func setupCodeRejectsTailnetPlaintextWs() {
let payload = #"{"url":"ws://gateway.tailnet.ts.net:18789","bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeRejectsCgnatPlaintextWs() {
let payload = #"{"url":"ws://100.64.0.9:18789","bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeParsesHostPayload() {
let payload = #"{"host":"gateway.tailnet.ts.net","port":443,"tls":true,"bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "gateway.tailnet.ts.net",
port: 443,
tls: true,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeParsesHostPayloadWithTLSDefaultPort() {
let payload = #"{"host":"gateway.tailnet.ts.net","tls":true,"bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "gateway.tailnet.ts.net",
port: 443,
tls: true,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeRejectsInsecureHostPayload() {
let payload = #"{"host":"gateway.tailnet.ts.net","port":18789,"tls":false,"bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeAllowsPrivateLanHostPayload() {
let payload = #"{"host":"openclaw.local","port":18789,"tls":false,"bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupInputParsesFullCopiedSetupMessage() {
let payload = #"{"url":"wss://gateway.tailnet.ts.net","bootstrapToken":"tok"}"#
let message = """
Pairing setup code generated.
Setup code:
\(setupCode(from: payload))
"""
#expect(
GatewayConnectDeepLink.fromSetupInput(message) == .init(
host: "gateway.tailnet.ts.net",
port: 443,
tls: true,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupInputParsesRawGatewayURL() {
#expect(
GatewayConnectDeepLink.fromSetupInput("wss://gateway.example.com:444") == .init(
host: "gateway.example.com",
port: 444,
tls: true,
bootstrapToken: nil,
token: nil,
password: nil))
}
}