mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 20:00:22 +00:00
fix(pairing): restore qr bootstrap onboarding handoff (#58382) (thanks @ngutman)
* fix(pairing): restore qr bootstrap onboarding handoff * fix(pairing): tighten bootstrap handoff follow-ups * fix(pairing): migrate legacy gateway device auth * fix(pairing): narrow qr bootstrap handoff scope * fix(pairing): clear ios tls trust on onboarding reset * fix(pairing): restore qr bootstrap onboarding handoff (#58382) (thanks @ngutman)
This commit is contained in:
@@ -1697,14 +1697,24 @@ extension NodeAppModel {
|
||||
password: password,
|
||||
nodeOptions: connectOptions)
|
||||
self.prepareForGatewayConnect(url: url, stableID: effectiveStableID)
|
||||
self.startOperatorGatewayLoop(
|
||||
url: url,
|
||||
stableID: effectiveStableID,
|
||||
if self.shouldStartOperatorGatewayLoop(
|
||||
token: token,
|
||||
bootstrapToken: bootstrapToken,
|
||||
password: password,
|
||||
nodeOptions: connectOptions,
|
||||
sessionBox: sessionBox)
|
||||
stableID: effectiveStableID)
|
||||
{
|
||||
self.startOperatorGatewayLoop(
|
||||
url: url,
|
||||
stableID: effectiveStableID,
|
||||
token: token,
|
||||
bootstrapToken: bootstrapToken,
|
||||
password: password,
|
||||
nodeOptions: connectOptions,
|
||||
sessionBox: sessionBox)
|
||||
} else {
|
||||
self.operatorGatewayTask = nil
|
||||
Task { await self.operatorGateway.disconnect() }
|
||||
}
|
||||
self.startNodeGatewayLoop(
|
||||
url: url,
|
||||
stableID: effectiveStableID,
|
||||
@@ -1785,6 +1795,86 @@ private extension NodeAppModel {
|
||||
self.apnsLastRegisteredTokenHex = nil
|
||||
}
|
||||
|
||||
func shouldStartOperatorGatewayLoop(
|
||||
token: String?,
|
||||
bootstrapToken: String?,
|
||||
password: String?,
|
||||
stableID _: String) -> Bool
|
||||
{
|
||||
Self.shouldStartOperatorGatewayLoop(
|
||||
token: token,
|
||||
bootstrapToken: bootstrapToken,
|
||||
password: password,
|
||||
hasStoredOperatorToken: self.hasStoredGatewayRoleToken("operator"))
|
||||
}
|
||||
|
||||
func hasStoredGatewayRoleToken(_ role: String) -> Bool {
|
||||
let identity = DeviceIdentityStore.loadOrCreate()
|
||||
return DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: role) != nil
|
||||
}
|
||||
|
||||
static func shouldStartOperatorGatewayLoop(
|
||||
token: String?,
|
||||
bootstrapToken: String?,
|
||||
password: String?,
|
||||
hasStoredOperatorToken: Bool) -> Bool
|
||||
{
|
||||
let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmedToken.isEmpty {
|
||||
return true
|
||||
}
|
||||
let trimmedPassword = password?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmedPassword.isEmpty {
|
||||
return true
|
||||
}
|
||||
let trimmedBootstrapToken = bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmedBootstrapToken.isEmpty {
|
||||
return false
|
||||
}
|
||||
return hasStoredOperatorToken
|
||||
}
|
||||
|
||||
static func clearingBootstrapToken(in config: GatewayConnectConfig?) -> GatewayConnectConfig? {
|
||||
guard let config else { return nil }
|
||||
let trimmedBootstrapToken = config.bootstrapToken?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !trimmedBootstrapToken.isEmpty else { return config }
|
||||
return GatewayConnectConfig(
|
||||
url: config.url,
|
||||
stableID: config.stableID,
|
||||
tls: config.tls,
|
||||
token: config.token,
|
||||
bootstrapToken: nil,
|
||||
password: config.password,
|
||||
nodeOptions: config.nodeOptions)
|
||||
}
|
||||
|
||||
func currentGatewayReconnectAuth(
|
||||
fallbackToken: String?,
|
||||
fallbackBootstrapToken: String?,
|
||||
fallbackPassword: String?) -> (token: String?, bootstrapToken: String?, password: String?)
|
||||
{
|
||||
if let cfg = self.activeGatewayConnectConfig {
|
||||
return (cfg.token, cfg.bootstrapToken, cfg.password)
|
||||
}
|
||||
return (fallbackToken, fallbackBootstrapToken, fallbackPassword)
|
||||
}
|
||||
|
||||
func clearPersistedGatewayBootstrapTokenIfNeeded() {
|
||||
// Always drop the in-memory bootstrap token after the first successful
|
||||
// bootstrap connect so reconnect loops cannot reuse a spent token.
|
||||
self.activeGatewayConnectConfig = Self.clearingBootstrapToken(in: self.activeGatewayConnectConfig)
|
||||
|
||||
let trimmedInstanceId = UserDefaults.standard.string(forKey: "node.instanceId")?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !trimmedInstanceId.isEmpty else { return }
|
||||
guard
|
||||
GatewaySettingsStore.loadGatewayBootstrapToken(instanceId: trimmedInstanceId) != nil
|
||||
else { return }
|
||||
|
||||
GatewaySettingsStore.clearGatewayBootstrapToken(instanceId: trimmedInstanceId)
|
||||
}
|
||||
|
||||
func refreshBackgroundReconnectSuppressionIfNeeded(source: String) {
|
||||
guard self.isBackgrounded else { return }
|
||||
guard !self.backgroundReconnectSuppressed else { return }
|
||||
@@ -1841,11 +1931,15 @@ private extension NodeAppModel {
|
||||
displayName: nodeOptions.clientDisplayName)
|
||||
|
||||
do {
|
||||
let reconnectAuth = self.currentGatewayReconnectAuth(
|
||||
fallbackToken: token,
|
||||
fallbackBootstrapToken: bootstrapToken,
|
||||
fallbackPassword: password)
|
||||
try await self.operatorGateway.connect(
|
||||
url: url,
|
||||
token: token,
|
||||
bootstrapToken: bootstrapToken,
|
||||
password: password,
|
||||
token: reconnectAuth.token,
|
||||
bootstrapToken: reconnectAuth.bootstrapToken,
|
||||
password: reconnectAuth.password,
|
||||
connectOptions: operatorOptions,
|
||||
sessionBox: sessionBox,
|
||||
onConnected: { [weak self] in
|
||||
@@ -1948,12 +2042,16 @@ private extension NodeAppModel {
|
||||
|
||||
do {
|
||||
let epochMs = Int(Date().timeIntervalSince1970 * 1000)
|
||||
let reconnectAuth = self.currentGatewayReconnectAuth(
|
||||
fallbackToken: token,
|
||||
fallbackBootstrapToken: bootstrapToken,
|
||||
fallbackPassword: password)
|
||||
GatewayDiagnostics.log("connect attempt epochMs=\(epochMs) url=\(url.absoluteString)")
|
||||
try await self.nodeGateway.connect(
|
||||
url: url,
|
||||
token: token,
|
||||
bootstrapToken: bootstrapToken,
|
||||
password: password,
|
||||
token: reconnectAuth.token,
|
||||
bootstrapToken: reconnectAuth.bootstrapToken,
|
||||
password: reconnectAuth.password,
|
||||
connectOptions: currentOptions,
|
||||
sessionBox: sessionBox,
|
||||
onConnected: { [weak self] in
|
||||
@@ -1965,6 +2063,30 @@ private extension NodeAppModel {
|
||||
self.screen.errorText = nil
|
||||
UserDefaults.standard.set(true, forKey: "gateway.autoconnect")
|
||||
}
|
||||
let usedBootstrapToken =
|
||||
reconnectAuth.token?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty != false &&
|
||||
reconnectAuth.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.isEmpty == false
|
||||
if usedBootstrapToken {
|
||||
await MainActor.run {
|
||||
self.clearPersistedGatewayBootstrapTokenIfNeeded()
|
||||
if self.operatorGatewayTask == nil && self.shouldStartOperatorGatewayLoop(
|
||||
token: reconnectAuth.token,
|
||||
bootstrapToken: nil,
|
||||
password: reconnectAuth.password,
|
||||
stableID: stableID)
|
||||
{
|
||||
self.startOperatorGatewayLoop(
|
||||
url: url,
|
||||
stableID: stableID,
|
||||
token: reconnectAuth.token,
|
||||
bootstrapToken: nil,
|
||||
password: reconnectAuth.password,
|
||||
nodeOptions: currentOptions,
|
||||
sessionBox: sessionBox)
|
||||
}
|
||||
}
|
||||
}
|
||||
let relayData = await MainActor.run {
|
||||
(
|
||||
sessionKey: self.mainSessionKey,
|
||||
@@ -1975,8 +2097,8 @@ private extension NodeAppModel {
|
||||
ShareGatewayRelaySettings.saveConfig(
|
||||
ShareGatewayRelayConfig(
|
||||
gatewayURLString: url.absoluteString,
|
||||
token: token,
|
||||
password: password,
|
||||
token: reconnectAuth.token,
|
||||
password: reconnectAuth.password,
|
||||
sessionKey: relayData.sessionKey,
|
||||
deliveryChannel: relayData.deliveryChannel,
|
||||
deliveryTo: relayData.deliveryTo))
|
||||
@@ -3015,6 +3137,20 @@ extension NodeAppModel {
|
||||
static func _test_currentDeepLinkKey() -> String {
|
||||
self.expectedDeepLinkKey()
|
||||
}
|
||||
|
||||
static func _test_shouldStartOperatorGatewayLoop(
|
||||
token: String?,
|
||||
bootstrapToken: String?,
|
||||
password: String?,
|
||||
hasStoredOperatorToken: Bool) -> Bool
|
||||
{
|
||||
self.shouldStartOperatorGatewayLoop(
|
||||
token: token,
|
||||
bootstrapToken: bootstrapToken,
|
||||
password: password,
|
||||
hasStoredOperatorToken: hasStoredOperatorToken)
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
// swiftlint:enable type_body_length file_length
|
||||
|
||||
Reference in New Issue
Block a user