mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-30 12:07:42 +00:00
fix(apple): accept raw IPv6 gateway setup addresses (#129416)
Bracket IPv6 authorities in the shared endpoint URL owner and derive Watch HTTPS from its validated WebSocket URL. Preserve stored hosts, credential namespaces, encoded paths, and TLS policy. Prove raw/bracketed/scoped hosts through shared setup parsing and Watch configuration regressions: base failures, then 47 shared and five Watch test functions passing with the repair.
This commit is contained in:
parent
51ad689168
commit
2fe1956ea1
5 changed files with 72 additions and 13 deletions
|
|
@ -67,12 +67,10 @@ struct WatchGatewayConfiguration: Codable {
|
|||
}
|
||||
|
||||
static func httpBaseURL(for link: GatewayConnectDeepLink) -> URL? {
|
||||
guard link.tls else { return nil }
|
||||
var components = URLComponents()
|
||||
guard link.tls, let websocketURL = link.websocketURL,
|
||||
var components = URLComponents(url: websocketURL, resolvingAgainstBaseURL: false)
|
||||
else { return nil }
|
||||
components.scheme = "https"
|
||||
components.host = link.host
|
||||
components.port = link.port
|
||||
components.percentEncodedPath = link.contextPath ?? ""
|
||||
return components.url
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,22 @@ import Testing
|
|||
@testable import OpenClawWatchApp
|
||||
|
||||
struct WatchGatewayConfigurationTests {
|
||||
@Test func `direct Watch setup retains the endpoint prefix across credential handoff`() throws {
|
||||
@Test(arguments: [
|
||||
("watch.example.invalid", "watch.example.invalid"),
|
||||
("127.0.0.1", "127.0.0.1"),
|
||||
("::1", "[::1]"),
|
||||
("[::1]", "[::1]"),
|
||||
("fd00::1", "[fd00::1]"),
|
||||
("[fd00::1]", "[fd00::1]"),
|
||||
("fe80::1%en0", "[fe80::1%25en0]"),
|
||||
("[fe80::1%en0]", "[fe80::1%25en0]"),
|
||||
])
|
||||
func `direct Watch setup retains the endpoint prefix across credential handoff`(
|
||||
host: String,
|
||||
authority: String) throws
|
||||
{
|
||||
let link = GatewayConnectDeepLink(
|
||||
host: "watch.example.invalid",
|
||||
host: host,
|
||||
port: 443,
|
||||
tls: true,
|
||||
contextPath: "/gateways/team%2Fwatch",
|
||||
|
|
@ -16,12 +29,13 @@ struct WatchGatewayConfigurationTests {
|
|||
let configuration = try #require(WatchGatewayConfiguration(setupLink: link, sentAtMs: 1))
|
||||
let paired = configuration.withoutBootstrapToken()
|
||||
|
||||
#expect(configuration.link.host == host)
|
||||
#expect(configuration.link.contextPath == "/gateways/team%2Fwatch")
|
||||
#expect(configuration.gatewayID == "watch-direct:https://watch.example.invalid:443/gateways/team%2Fwatch")
|
||||
#expect(configuration.gatewayID == "watch-direct:https://" + host.lowercased() + ":443/gateways/team%2Fwatch")
|
||||
#expect(WatchGatewayConfiguration.httpBaseURL(for: paired.link)?.absoluteString
|
||||
== "https://watch.example.invalid:443/gateways/team%2Fwatch")
|
||||
== "https://" + authority + ":443/gateways/team%2Fwatch")
|
||||
#expect(paired.voiceConnection.websocketURLs.first?.absoluteString
|
||||
== "wss://watch.example.invalid:443/gateways/team%2Fwatch")
|
||||
== "wss://" + authority + ":443/gateways/team%2Fwatch")
|
||||
#expect(paired.link.bootstrapToken == nil)
|
||||
|
||||
let restored = try JSONDecoder().decode(
|
||||
|
|
@ -33,6 +47,7 @@ struct WatchGatewayConfigurationTests {
|
|||
@Test func `direct Watch setup selects a secure endpoint without changing its namespace`() throws {
|
||||
let link = try #require(GatewayConnectDeepLink.fromSetupCode(
|
||||
#"{"url":"ws://192.168.1.2:18789","urls":["wss://watch.example.invalid/team-a","wss://backup.example.invalid/team-b"],"bootstrapToken":"one-time-setup"}"#))
|
||||
#expect(WatchGatewayConfiguration.httpBaseURL(for: link) == nil)
|
||||
let configuration = try #require(WatchGatewayConfiguration(setupLink: link, sentAtMs: 2))
|
||||
#expect(configuration.link.contextPath == "/team-a")
|
||||
#expect(configuration.voiceConnection.websocketURLs.map(\.absoluteString)
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ public struct GatewayConnectEndpoint: Codable, Sendable, Equatable {
|
|||
guard (1...65535).contains(self.port) else { return nil }
|
||||
var components = URLComponents()
|
||||
components.scheme = self.tls ? "wss" : "ws"
|
||||
components.host = self.host
|
||||
components.host = self.host.contains(":") && !self.host.hasPrefix("[") ? "[" + self.host + "]" : self.host
|
||||
components.port = self.port
|
||||
components.percentEncodedPath = self.contextPath ?? ""
|
||||
return components.url
|
||||
|
|
|
|||
|
|
@ -19,6 +19,27 @@ private func gatewayLink(from raw: String) -> GatewayConnectDeepLink? {
|
|||
}
|
||||
|
||||
@Suite struct DeepLinksSecurityTests {
|
||||
@Test(arguments: [
|
||||
("::1", false, "ws://[::1]:18789"),
|
||||
("fd00::1", false, "ws://[fd00::1]:18789"),
|
||||
("fd00::1", true, "wss://[fd00::1]:18789"),
|
||||
("[::1]", false, "ws://[::1]:18789"),
|
||||
("[fd00::1]", true, "wss://[fd00::1]:18789"),
|
||||
("fe80::1%en0", false, "ws://[fe80::1%25en0]:18789"),
|
||||
("[fe80::1%en0]", false, "ws://[fe80::1%25en0]:18789"),
|
||||
("127.0.0.1", false, "ws://127.0.0.1:18789"),
|
||||
("gateway.example", true, "wss://gateway.example:18789"),
|
||||
])
|
||||
func gatewayEndpointFormatsRawAndBracketedHosts(host: String, tls: Bool, expectedURL: String) {
|
||||
let contextPaths: [String?] = [nil, "/gateways/team%2Fwatch"]
|
||||
for contextPath in contextPaths {
|
||||
let endpoint = GatewayConnectEndpoint(host: host, port: 18789, tls: tls, contextPath: contextPath)
|
||||
|
||||
#expect(endpoint.host == host)
|
||||
#expect(endpoint.websocketURL?.absoluteString == expectedURL + (contextPath ?? ""))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func setupResultInitializerDefaultsOptionalFields() {
|
||||
let result = DevicePairSetupCodeResult(
|
||||
setupid: "setup-1",
|
||||
|
|
@ -412,6 +433,26 @@ private func gatewayLink(from raw: String) -> GatewayConnectDeepLink? {
|
|||
password: nil))
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
("::1", false, "ws://[::1]:18789"),
|
||||
("fd00::1", false, "ws://[fd00::1]:18789"),
|
||||
("fd00::1", true, "wss://[fd00::1]:18789"),
|
||||
("[fd00::1]", false, "ws://[fd00::1]:18789"),
|
||||
("fe80::1%en0", false, "ws://[fe80::1%25en0]:18789"),
|
||||
("2001:db8::1", true, "wss://[2001:db8::1]:18789"),
|
||||
])
|
||||
func setupCodeParsesIPv6HostPayload(host: String, tls: Bool, expectedURL: String) {
|
||||
let payload = #"{"host":"\#(host)","port":18789,"tls":\#(tls),"bootstrapToken":"tok"}"#
|
||||
|
||||
for input in [payload, setupCode(from: payload)] {
|
||||
let link = GatewayConnectDeepLink.fromSetupInput(input)
|
||||
|
||||
#expect(link?.host == host)
|
||||
#expect(link?.bootstrapToken == "tok")
|
||||
#expect(link?.websocketURL?.absoluteString == expectedURL)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func setupCodeParsesHostPayloadWithTLSDefaultPort() {
|
||||
let payload = #"{"host":"gateway.tailnet.ts.net","tls":true,"bootstrapToken":"tok"}"#
|
||||
#expect(
|
||||
|
|
@ -424,8 +465,9 @@ private func gatewayLink(from raw: String) -> GatewayConnectDeepLink? {
|
|||
password: nil))
|
||||
}
|
||||
|
||||
@Test func setupCodeRejectsInsecureHostPayload() {
|
||||
let payload = #"{"host":"gateway.tailnet.ts.net","port":18789,"tls":false,"bootstrapToken":"tok"}"#
|
||||
@Test(arguments: ["gateway.tailnet.ts.net", "2001:db8::1", "[2001:db8::1]"])
|
||||
func setupCodeRejectsInsecureHostPayload(host: String) {
|
||||
let payload = #"{"host":"\#(host)","port":18789,"tls":false,"bootstrapToken":"tok"}"#
|
||||
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -727,6 +727,10 @@ If mDNS is blocked, use a unicast DNS-SD zone (choose a domain; example: `opencl
|
|||
|
||||
Open **Settings → Gateway**, enable **Use Manual Gateway**, and enter the Gateway host + port (default `18789`).
|
||||
|
||||
IPv6 host fields accept raw addresses such as `fd00::1` and bracketed addresses
|
||||
such as `[fd00::1]`. Full Gateway URLs require brackets around the address,
|
||||
for example `wss://[fd00::1]:18789`.
|
||||
|
||||
## Multiple Gateways
|
||||
|
||||
The app keeps a registry of every Gateway it has paired with, so you can switch between them without pairing again:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue