mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 11:36:06 +00:00
263 lines
9.6 KiB
Swift
263 lines
9.6 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 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","urls":["ws://192.168.1.20:18789","wss://gateway.tailnet.ts.net:8443"],"bootstrapToken":"tok"}"#
|
|
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
|
|
|
|
#expect(link?.connectionEndpoints == [
|
|
.init(host: "192.168.1.20", port: 18789, tls: false),
|
|
.init(host: "gateway.tailnet.ts.net", port: 8443, tls: true),
|
|
])
|
|
#expect(try link?.selectingEndpoint(#require(link?.connectionEndpoints[1])) == .init(
|
|
host: "gateway.tailnet.ts.net",
|
|
port: 8443,
|
|
tls: true,
|
|
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.fallbackEndpoints.isEmpty)
|
|
}
|
|
|
|
@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))
|
|
}
|
|
}
|