/
FxADeviceRegistration.swift
184 lines (160 loc) · 8.38 KB
/
FxADeviceRegistration.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Deferred
import Shared
import SwiftyJSON
private let log = Logger.syncLogger
/// The current version of the device registration. We use this to re-register
/// devices after we update what we send on device registration.
private let DeviceRegistrationVersion = 1
public enum FxADeviceRegistrationResult {
case registered
case updated
case alreadyRegistered
}
public enum FxADeviceRegistratorError: MaybeErrorType {
case accountDeleted
case currentDeviceNotFound
case invalidSession
case unknownDevice
public var description: String {
switch self {
case .accountDeleted: return "Account no longer exists."
case .currentDeviceNotFound: return "Current device not found."
case .invalidSession: return "Session token was invalid."
case .unknownDevice: return "Unknown device."
}
}
}
open class FxADeviceRegistration: NSObject, NSCoding {
/// The device identifier identifying this device. A device is uniquely identified
/// across the lifetime of a Firefox Account.
public let id: String
/// The version of the device registration. We use this to re-register
/// devices after we update what we send on device registration.
let version: Int
/// The last time we successfully (re-)registered with the server.
let lastRegistered: Timestamp
init(id: String, version: Int, lastRegistered: Timestamp) {
self.id = id
self.version = version
self.lastRegistered = lastRegistered
}
public convenience required init(coder: NSCoder) {
let id = coder.decodeObject(forKey: "id") as! String
let version = coder.decodeAsInt(forKey: "version")
let lastRegistered = coder.decodeAsUInt64(forKey: "lastRegistered")
self.init(id: id, version: version, lastRegistered: lastRegistered)
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(version, forKey: "version")
aCoder.encode(NSNumber(value: lastRegistered), forKey: "lastRegistered")
}
open func toJSON() -> JSON {
return JSON(object: [
"id": id,
"version": version,
"lastRegistered": lastRegistered,
])
}
}
open class FxADeviceRegistrator {
open static func registerOrUpdateDevice(_ account: FirefoxAccount, sessionToken: NSData, client: FxAClient10? = nil) -> Deferred<Maybe<FxADeviceRegistrationResult>> {
// If we've already registered, the registration version is up-to-date, *and* we've (re-)registered
// within the last week, do nothing. We re-register weekly as a sanity check.
if let registration = account.deviceRegistration, registration.version == DeviceRegistrationVersion &&
Date.now() < registration.lastRegistered + OneWeekInMilliseconds {
return deferMaybe(FxADeviceRegistrationResult.alreadyRegistered)
}
let pushParams: FxADevicePushParams?
if AppConstants.MOZ_FXA_PUSH, let pushRegistration = account.pushRegistration {
let subscription = pushRegistration.defaultSubscription
pushParams = FxADevicePushParams(callback: subscription.endpoint.absoluteString, publicKey: subscription.p256dhPublicKey, authKey: subscription.authKey)
} else {
pushParams = nil
}
let client = client ?? FxAClient10(endpoint: account.configuration.authEndpointURL)
let name = DeviceInfo.defaultClientName()
let device: FxADevice
let registrationResult: FxADeviceRegistrationResult
if let registration = account.deviceRegistration {
device = FxADevice.forUpdate(name, id: registration.id, push: pushParams)
registrationResult = FxADeviceRegistrationResult.updated
} else {
device = FxADevice.forRegister(name, type: "mobile", push: pushParams)
registrationResult = FxADeviceRegistrationResult.registered
}
let registeredDevice = client.registerOrUpdate(device: device, withSessionToken: sessionToken)
let registration: Deferred<Maybe<FxADeviceRegistration>> = registeredDevice.bind { result in
if let device = result.successValue {
return deferMaybe(FxADeviceRegistration(id: device.id!, version: DeviceRegistrationVersion, lastRegistered: Date.now()))
}
// Recover from the error -- if we can.
if let error = result.failureValue as? FxAClientError,
case .remote(let remoteError) = error {
switch remoteError.code {
case FxAccountRemoteError.DeviceSessionConflict:
return recoverFromDeviceSessionConflict(account, client: client, sessionToken: sessionToken)
case FxAccountRemoteError.InvalidAuthenticationToken:
return recoverFromTokenError(account, client: client)
case FxAccountRemoteError.UnknownDevice:
return recoverFromUnknownDevice(account)
default: break
}
}
// Not an error we can recover from. Rethrow it and fall back to the failure handler.
return deferMaybe(result.failureValue!)
}
// Post-recovery. We either registered or we didn't, but update the account either way.
return registration.bind { result in
switch result {
case .success(let registration):
account.deviceRegistration = registration.value
return deferMaybe(registrationResult)
case .failure(let error):
log.error("Device registration failed: \(error.description)")
if let registration = account.deviceRegistration {
account.deviceRegistration = FxADeviceRegistration(id: registration.id, version: 0, lastRegistered: registration.lastRegistered)
}
return deferMaybe(error)
}
}
}
fileprivate static func recoverFromDeviceSessionConflict(_ account: FirefoxAccount, client: FxAClient10, sessionToken: NSData) -> Deferred<Maybe<FxADeviceRegistration>> {
// FxA has already associated this session with a different device id.
// Perhaps we were beaten in a race to register. Handle the conflict:
// 1. Fetch the list of devices for the current user from FxA.
// 2. Look for ourselves in the list.
// 3. If we find a match, set the correct device id and device registration
// version on the account data and return the correct device id. At next
// sync or next sign-in, registration is retried and should succeed.
log.warning("Device session conflict. Attempting to find the current device ID…")
return client.devices(withSessionToken: sessionToken) >>== { response in
guard let currentDevice = response.devices.find({ $0.isCurrentDevice }) else {
return deferMaybe(FxADeviceRegistratorError.currentDeviceNotFound)
}
return deferMaybe(FxADeviceRegistration(id: currentDevice.id!, version: 0, lastRegistered: Date.now()))
}
}
fileprivate static func recoverFromTokenError(_ account: FirefoxAccount, client: FxAClient10) -> Deferred<Maybe<FxADeviceRegistration>> {
return client.status(forUID: account.uid) >>== { status in
_ = account.makeDoghouse()
if !status.exists {
// TODO: Should be in an "I have an iOS account, but the FxA is gone." state.
// This will do for now...
return deferMaybe(FxADeviceRegistratorError.accountDeleted)
}
return deferMaybe(FxADeviceRegistratorError.invalidSession)
}
}
fileprivate static func recoverFromUnknownDevice(_ account: FirefoxAccount) -> Deferred<Maybe<FxADeviceRegistration>> {
// FxA did not recognize the device ID. Handle it by clearing the registration on the account data.
// At next sync or next sign-in, registration is retried and should succeed.
log.warning("Unknown device ID. Clearing the local device data.")
account.deviceRegistration = nil
return deferMaybe(FxADeviceRegistratorError.unknownDevice)
}
}