mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-11 04:48:05 +00:00
fix(ios): recover rotated gateway certificates
## Summary - allow iOS to trust system-valid rotated gateway certificates - rebuild active gateway sessions after replacing the stored TLS pin - expose certificate trust recovery from gateway problem banners ## Verification - swift test --filter 'GatewayErrorsTests|GatewayNodeSessionTests/changedSessionBoxRebuildsExistingGatewayChannel' - xcodebuild build -scheme OpenClaw -destination 'platform=iOS,id=00008140-000848A92EE3001C' - installed and launched OpenClaw on attached iPhone with devicectl - verified iOS gateway log connected to wss://gutsy-home.tail06a72.ts.net:443 after trust/pairing recovery
This commit is contained in:
@@ -295,6 +295,47 @@ final class GatewayConnectionController {
|
||||
self.appModel?.gatewayStatusText = "Offline"
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func trustRotatedGatewayCertificate(from problem: GatewayConnectionProblem) async -> Bool {
|
||||
guard problem.canTrustRotatedCertificate,
|
||||
let stableID = problem.tlsStoreKey,
|
||||
let fingerprint = problem.tlsObservedFingerprint
|
||||
else {
|
||||
self.appModel?.gatewayStatusText = "Certificate review required"
|
||||
return false
|
||||
}
|
||||
|
||||
guard GatewayTLSStore.replaceFingerprint(fingerprint, stableID: stableID) else {
|
||||
self.appModel?.gatewayStatusText = "Could not update gateway certificate"
|
||||
return false
|
||||
}
|
||||
|
||||
GatewayDiagnostics.log(
|
||||
"gateway tls pin replaced stableID=\(stableID) "
|
||||
+ "old=\(problem.tlsExpectedFingerprint ?? "unknown") new=\(fingerprint)")
|
||||
self.appModel?.gatewayStatusText = "Gateway certificate updated. Reconnecting…"
|
||||
if let appModel = self.appModel, let cfg = appModel.activeGatewayConnectConfig {
|
||||
let currentTLS = cfg.tls
|
||||
let refreshedTLS = GatewayTLSParams(
|
||||
required: currentTLS?.required ?? true,
|
||||
expectedFingerprint: fingerprint,
|
||||
allowTOFU: currentTLS?.allowTOFU ?? false,
|
||||
storeKey: currentTLS?.storeKey ?? stableID)
|
||||
let refreshedConfig = GatewayConnectConfig(
|
||||
url: cfg.url,
|
||||
stableID: cfg.stableID,
|
||||
tls: refreshedTLS,
|
||||
token: cfg.token,
|
||||
bootstrapToken: cfg.bootstrapToken,
|
||||
password: cfg.password,
|
||||
nodeOptions: cfg.nodeOptions)
|
||||
appModel.applyGatewayConnectConfig(refreshedConfig)
|
||||
} else {
|
||||
await self.connectLastKnown()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func updateFromDiscovery() {
|
||||
let newGateways = self.discovery.gateways
|
||||
self.gateways = newGateways
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
|
||||
struct GatewayQuickSetupSheet: View {
|
||||
@@ -19,6 +20,10 @@ struct GatewayQuickSetupSheet: View {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemBanner(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) }
|
||||
},
|
||||
onShowDetails: {
|
||||
self.showGatewayProblemDetails = true
|
||||
})
|
||||
@@ -115,7 +120,12 @@ struct GatewayQuickSetupSheet: View {
|
||||
}
|
||||
.sheet(isPresented: self.$showGatewayProblemDetails) {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemDetailsSheet(problem: gatewayProblem)
|
||||
GatewayProblemDetailsSheet(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,4 +134,21 @@ struct GatewayQuickSetupSheet: View {
|
||||
// Prefer whatever discovery says is first; the list is already name-sorted.
|
||||
self.gatewayController.gateways.first
|
||||
}
|
||||
|
||||
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
|
||||
problem.canTrustRotatedCertificate ? "Trust certificate" : "Connect"
|
||||
}
|
||||
|
||||
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async {
|
||||
if problem.canTrustRotatedCertificate {
|
||||
_ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem)
|
||||
return
|
||||
}
|
||||
guard let candidate = self.bestCandidate else { return }
|
||||
self.connectError = nil
|
||||
self.connecting = true
|
||||
let err = await self.gatewayController.connectWithDiagnostics(candidate)
|
||||
self.connecting = false
|
||||
self.connectError = err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,9 +217,9 @@ struct OnboardingWizardView: View {
|
||||
if let currentProblem = self.currentProblem {
|
||||
GatewayProblemDetailsSheet(
|
||||
problem: currentProblem,
|
||||
primaryActionTitle: "Retry",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(currentProblem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.retryLastAttempt() }
|
||||
Task { await self.handleGatewayProblemPrimaryAction(currentProblem) }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -594,9 +594,9 @@ struct OnboardingWizardView: View {
|
||||
if let problem = self.currentProblem {
|
||||
GatewayProblemBanner(
|
||||
problem: problem,
|
||||
primaryActionTitle: "Retry connection",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(problem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.retryLastAttempt() }
|
||||
Task { await self.handleGatewayProblemPrimaryAction(problem) }
|
||||
},
|
||||
onShowDetails: {
|
||||
self.showGatewayProblemDetails = true
|
||||
@@ -1014,6 +1014,22 @@ struct OnboardingWizardView: View {
|
||||
defer { self.connectingGatewayID = nil }
|
||||
await self.gatewayController.connectLastKnown()
|
||||
}
|
||||
|
||||
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
|
||||
problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
|
||||
}
|
||||
|
||||
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async {
|
||||
if problem.canTrustRotatedCertificate {
|
||||
self.connectingGatewayID = "trust-certificate"
|
||||
self.connectMessage = "Updating gateway certificate…"
|
||||
self.statusLine = "Updating gateway certificate…"
|
||||
defer { self.connectingGatewayID = nil }
|
||||
_ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem)
|
||||
return
|
||||
}
|
||||
await self.retryLastAttempt()
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingModeRow: View {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
@@ -454,6 +455,7 @@ private struct HomeCanvasAgentCard: Codable {
|
||||
|
||||
private struct CanvasContent: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
@Environment(GatewayConnectionController.self) private var gatewayController
|
||||
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
|
||||
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
|
||||
@State private var showGatewayActions: Bool = false
|
||||
@@ -522,13 +524,9 @@ private struct CanvasContent: View {
|
||||
{
|
||||
GatewayProblemBanner(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: gatewayProblem.retryable ? "Retry" : "Open Settings",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
if gatewayProblem.retryable {
|
||||
self.retryGatewayConnection()
|
||||
} else {
|
||||
self.openSettings()
|
||||
}
|
||||
self.handleGatewayProblemPrimaryAction(gatewayProblem)
|
||||
},
|
||||
onShowDetails: {
|
||||
self.showGatewayProblemDetails = true
|
||||
@@ -556,9 +554,9 @@ private struct CanvasContent: View {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemDetailsSheet(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: "Open Settings",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
self.openSettings()
|
||||
self.handleGatewayProblemPrimaryAction(gatewayProblem)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -577,6 +575,21 @@ private struct CanvasContent: View {
|
||||
cameraHUDText: self.cameraHUDText,
|
||||
cameraHUDKind: self.cameraHUDKind)
|
||||
}
|
||||
|
||||
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
|
||||
if problem.canTrustRotatedCertificate { return "Trust certificate" }
|
||||
return problem.retryable ? "Retry" : "Open Settings"
|
||||
}
|
||||
|
||||
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) {
|
||||
if problem.canTrustRotatedCertificate {
|
||||
Task { await self.gatewayController.trustRotatedGatewayCertificate(from: problem) }
|
||||
} else if problem.retryable {
|
||||
self.retryGatewayConnection()
|
||||
} else {
|
||||
self.openSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CameraFlashOverlay: View {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
|
||||
struct RootTabs: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
@Environment(VoiceWakeManager.self) private var voiceWake
|
||||
@Environment(GatewayConnectionController.self) private var gatewayController
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false
|
||||
@State private var selectedTab: Int = 0
|
||||
@@ -48,9 +50,9 @@ struct RootTabs: View {
|
||||
{
|
||||
GatewayProblemBanner(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: "Open Settings",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
self.selectedTab = 2
|
||||
self.handleGatewayProblemPrimaryAction(gatewayProblem)
|
||||
},
|
||||
onShowDetails: {
|
||||
self.showGatewayProblemDetails = true
|
||||
@@ -99,9 +101,9 @@ struct RootTabs: View {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemDetailsSheet(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: "Open Settings",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
self.selectedTab = 2
|
||||
self.handleGatewayProblemPrimaryAction(gatewayProblem)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -118,4 +120,16 @@ struct RootTabs: View {
|
||||
cameraHUDText: self.appModel.cameraHUDText,
|
||||
cameraHUDKind: self.appModel.cameraHUDKind)
|
||||
}
|
||||
|
||||
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
|
||||
problem.canTrustRotatedCertificate ? "Trust certificate" : "Open Settings"
|
||||
}
|
||||
|
||||
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) {
|
||||
if problem.canTrustRotatedCertificate {
|
||||
Task { await self.gatewayController.trustRotatedGatewayCertificate(from: problem) }
|
||||
} else {
|
||||
self.selectedTab = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +72,9 @@ struct SettingsTab: View {
|
||||
{
|
||||
GatewayProblemBanner(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: "Retry connection",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.retryGatewayConnectionFromProblem() }
|
||||
Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) }
|
||||
},
|
||||
onShowDetails: {
|
||||
self.showGatewayProblemDetails = true
|
||||
@@ -433,9 +433,9 @@ struct SettingsTab: View {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemDetailsSheet(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: "Retry",
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.retryGatewayConnectionFromProblem() }
|
||||
Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1062,6 +1062,18 @@ struct SettingsTab: View {
|
||||
await self.connectLastKnown()
|
||||
}
|
||||
|
||||
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
|
||||
problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
|
||||
}
|
||||
|
||||
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async {
|
||||
if problem.canTrustRotatedCertificate {
|
||||
_ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem)
|
||||
return
|
||||
}
|
||||
await self.retryGatewayConnectionFromProblem()
|
||||
}
|
||||
|
||||
private func resetOnboarding() {
|
||||
// Disconnect first so RootCanvas doesn't instantly mark onboarding complete again.
|
||||
self.appModel.disconnectGateway()
|
||||
|
||||
@@ -155,4 +155,48 @@ import Testing
|
||||
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID1) == nil)
|
||||
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID2) == nil)
|
||||
}
|
||||
|
||||
@Test func trustedPinMismatchCanBeRecoveredByReplacingStoredPin() {
|
||||
let stableID = "test|\(UUID().uuidString)"
|
||||
defer { GatewayTLSStore.clearFingerprint(stableID: stableID) }
|
||||
GatewayTLSStore.saveFingerprint("old", stableID: stableID)
|
||||
|
||||
let error = GatewayTLSValidationError(
|
||||
failure: GatewayTLSValidationFailure(
|
||||
kind: .pinMismatch,
|
||||
host: "gateway.tailnet.ts.net",
|
||||
storeKey: stableID,
|
||||
expectedFingerprint: "old",
|
||||
observedFingerprint: "new",
|
||||
systemTrustOk: true),
|
||||
context: "connect to gateway")
|
||||
|
||||
let problem = GatewayConnectionProblemMapper.map(error: error)
|
||||
|
||||
#expect(problem?.kind == .tlsPinMismatch)
|
||||
#expect(problem?.canTrustRotatedCertificate == true)
|
||||
#expect(problem?.tlsStoreKey == stableID)
|
||||
#expect(problem?.tlsExpectedFingerprint == "old")
|
||||
#expect(problem?.tlsObservedFingerprint == "new")
|
||||
|
||||
#expect(GatewayTLSStore.replaceFingerprint(problem?.tlsObservedFingerprint ?? "", stableID: stableID))
|
||||
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID) == "new")
|
||||
}
|
||||
|
||||
@Test func untrustedPinMismatchCannotBeRecoveredInApp() {
|
||||
let error = GatewayTLSValidationError(
|
||||
failure: GatewayTLSValidationFailure(
|
||||
kind: .pinMismatch,
|
||||
host: "gateway.tailnet.ts.net",
|
||||
storeKey: "gateway",
|
||||
expectedFingerprint: "old",
|
||||
observedFingerprint: "new",
|
||||
systemTrustOk: false),
|
||||
context: "connect to gateway")
|
||||
|
||||
let problem = GatewayConnectionProblemMapper.map(error: error)
|
||||
|
||||
#expect(problem?.kind == .tlsPinMismatch)
|
||||
#expect(problem?.canTrustRotatedCertificate == false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ public struct GatewayConnectionProblem: Equatable, Sendable {
|
||||
public let retryable: Bool
|
||||
public let pauseReconnect: Bool
|
||||
public let technicalDetails: String?
|
||||
public let tlsStoreKey: String?
|
||||
public let tlsExpectedFingerprint: String?
|
||||
public let tlsObservedFingerprint: String?
|
||||
public let tlsSystemTrustOk: Bool
|
||||
|
||||
public init(
|
||||
kind: Kind,
|
||||
@@ -67,7 +71,11 @@ public struct GatewayConnectionProblem: Equatable, Sendable {
|
||||
requestId: String? = nil,
|
||||
retryable: Bool,
|
||||
pauseReconnect: Bool,
|
||||
technicalDetails: String? = nil)
|
||||
technicalDetails: String? = nil,
|
||||
tlsStoreKey: String? = nil,
|
||||
tlsExpectedFingerprint: String? = nil,
|
||||
tlsObservedFingerprint: String? = nil,
|
||||
tlsSystemTrustOk: Bool = false)
|
||||
{
|
||||
self.kind = kind
|
||||
self.owner = owner
|
||||
@@ -80,6 +88,10 @@ public struct GatewayConnectionProblem: Equatable, Sendable {
|
||||
self.retryable = retryable
|
||||
self.pauseReconnect = pauseReconnect
|
||||
self.technicalDetails = Self.trimmedOrNil(technicalDetails)
|
||||
self.tlsStoreKey = Self.trimmedOrNil(tlsStoreKey)
|
||||
self.tlsExpectedFingerprint = Self.trimmedOrNil(tlsExpectedFingerprint)
|
||||
self.tlsObservedFingerprint = Self.trimmedOrNil(tlsObservedFingerprint)
|
||||
self.tlsSystemTrustOk = tlsSystemTrustOk
|
||||
}
|
||||
|
||||
public var needsPairingApproval: Bool {
|
||||
@@ -121,6 +133,13 @@ public struct GatewayConnectionProblem: Equatable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public var canTrustRotatedCertificate: Bool {
|
||||
self.kind == .tlsPinMismatch
|
||||
&& self.tlsSystemTrustOk
|
||||
&& self.tlsStoreKey != nil
|
||||
&& self.tlsObservedFingerprint != nil
|
||||
}
|
||||
|
||||
private static func trimmedOrNil(_ value: String?) -> String? {
|
||||
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
@@ -541,7 +560,11 @@ public enum GatewayConnectionProblemMapper {
|
||||
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
|
||||
retryable: false,
|
||||
pauseReconnect: true,
|
||||
technicalDetails: tlsError.localizedDescription)
|
||||
technicalDetails: tlsError.localizedDescription,
|
||||
tlsStoreKey: failure.storeKey,
|
||||
tlsExpectedFingerprint: failure.expectedFingerprint,
|
||||
tlsObservedFingerprint: failure.observedFingerprint,
|
||||
tlsSystemTrustOk: failure.systemTrustOk)
|
||||
case .certificateUnavailable:
|
||||
return GatewayConnectionProblem(
|
||||
kind: .tlsCertificateUnavailable,
|
||||
|
||||
@@ -53,6 +53,7 @@ public actor GatewayNodeSession {
|
||||
private var activeBootstrapToken: String?
|
||||
private var activePassword: String?
|
||||
private var activeConnectOptionsKey: String?
|
||||
private var activeSessionIdentity: ObjectIdentifier?
|
||||
private var connectOptions: GatewayConnectOptions?
|
||||
private var onConnected: (@Sendable () async -> Void)?
|
||||
private var onDisconnected: (@Sendable (String) async -> Void)?
|
||||
@@ -195,11 +196,13 @@ public actor GatewayNodeSession {
|
||||
onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse) async throws
|
||||
{
|
||||
let nextOptionsKey = self.connectOptionsKey(connectOptions)
|
||||
let nextSessionIdentity = sessionBox.map { ObjectIdentifier($0.session) }
|
||||
let shouldReconnect = self.activeURL != url ||
|
||||
self.activeToken != token ||
|
||||
self.activeBootstrapToken != bootstrapToken ||
|
||||
self.activePassword != password ||
|
||||
self.activeConnectOptionsKey != nextOptionsKey ||
|
||||
self.activeSessionIdentity != nextSessionIdentity ||
|
||||
self.channel == nil
|
||||
|
||||
self.connectOptions = connectOptions
|
||||
@@ -231,6 +234,7 @@ public actor GatewayNodeSession {
|
||||
self.activeBootstrapToken = bootstrapToken
|
||||
self.activePassword = password
|
||||
self.activeConnectOptionsKey = nextOptionsKey
|
||||
self.activeSessionIdentity = nextSessionIdentity
|
||||
}
|
||||
|
||||
guard let channel = self.channel else {
|
||||
@@ -256,6 +260,7 @@ public actor GatewayNodeSession {
|
||||
self.activeBootstrapToken = nil
|
||||
self.activePassword = nil
|
||||
self.activeConnectOptionsKey = nil
|
||||
self.activeSessionIdentity = nil
|
||||
self.hasEverConnected = false
|
||||
self.resetConnectionState()
|
||||
}
|
||||
|
||||
@@ -107,6 +107,10 @@ import Testing
|
||||
#expect(problem?.retryable == false)
|
||||
#expect(problem?.pauseReconnect == true)
|
||||
#expect(problem?.actionLabel == "Review certificate")
|
||||
#expect(problem?.canTrustRotatedCertificate == true)
|
||||
#expect(problem?.tlsStoreKey == "gateway.example.ts.net:443")
|
||||
#expect(problem?.tlsExpectedFingerprint == "old")
|
||||
#expect(problem?.tlsObservedFingerprint == "new")
|
||||
}
|
||||
|
||||
@Test func untrustedTLSCertificatePausesReconnect() {
|
||||
@@ -126,4 +130,21 @@ import Testing
|
||||
#expect(problem?.retryable == false)
|
||||
#expect(problem?.pauseReconnect == true)
|
||||
}
|
||||
|
||||
@Test func untrustedTLSMismatchCannotBeRecoveredInApp() {
|
||||
let error = GatewayTLSValidationError(
|
||||
failure: GatewayTLSValidationFailure(
|
||||
kind: .pinMismatch,
|
||||
host: "gateway.example.ts.net",
|
||||
storeKey: "gateway.example.ts.net:443",
|
||||
expectedFingerprint: "old",
|
||||
observedFingerprint: "new",
|
||||
systemTrustOk: false),
|
||||
context: "connect to gateway")
|
||||
|
||||
let problem = GatewayConnectionProblemMapper.map(error: error)
|
||||
|
||||
#expect(problem?.kind == .tlsPinMismatch)
|
||||
#expect(problem?.canTrustRotatedCertificate == false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,54 @@ struct GatewayNodeSessionTests {
|
||||
await gateway.disconnect()
|
||||
}
|
||||
|
||||
@Test
|
||||
func changedSessionBoxRebuildsExistingGatewayChannel() async throws {
|
||||
let firstSession = FakeGatewayWebSocketSession()
|
||||
let secondSession = FakeGatewayWebSocketSession()
|
||||
let gateway = GatewayNodeSession()
|
||||
let options = GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios-test",
|
||||
clientMode: "node",
|
||||
clientDisplayName: "iOS Test",
|
||||
includeDeviceIdentity: false)
|
||||
|
||||
try await gateway.connect(
|
||||
url: URL(string: "wss://example.invalid")!,
|
||||
token: "shared-token",
|
||||
bootstrapToken: nil,
|
||||
password: nil,
|
||||
connectOptions: options,
|
||||
sessionBox: WebSocketSessionBox(session: firstSession),
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { req in
|
||||
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
|
||||
})
|
||||
|
||||
try await gateway.connect(
|
||||
url: URL(string: "wss://example.invalid")!,
|
||||
token: "shared-token",
|
||||
bootstrapToken: nil,
|
||||
password: nil,
|
||||
connectOptions: options,
|
||||
sessionBox: WebSocketSessionBox(session: secondSession),
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { req in
|
||||
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
|
||||
})
|
||||
|
||||
#expect(firstSession.snapshotMakeCount() == 1)
|
||||
#expect(secondSession.snapshotMakeCount() == 1)
|
||||
|
||||
await gateway.disconnect()
|
||||
}
|
||||
|
||||
@Test
|
||||
func bootstrapHelloStoresAdditionalDeviceTokens() async throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
|
||||
Reference in New Issue
Block a user