Revision control

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
import Foundation
6
import Shared
7
import Account
8
import XCGLogger
9
10
private let log = Logger.syncLogger
11
12
private let StorageVersionCurrent = 5
13
14
// Names of collections that can be enabled/disabled locally.
15
public let TogglableEngines: [String] = [
16
"bookmarks",
17
"history",
18
"tabs",
19
"passwords"
20
]
21
22
// Names of collections for which a synchronizer is implemented locally.
23
private let LocalEngines: [String] = TogglableEngines + ["clients"]
24
25
// Names of collections which will appear in a default meta/global produced locally.
26
// Map collection name to engine version. See http://docs.services.mozilla.com/sync/objectformats.html.
27
private let DefaultEngines: [String: Int] = [
28
"bookmarks": 2,
29
"clients": ClientsStorageVersion,
30
"history": HistoryStorageVersion,
31
"tabs": TabsStorageVersion,
32
// We opt-in to syncing collections we don't know about, since no client offers to sync non-enabled,
33
// non-declined engines yet. See Bug 969669.
34
"passwords": 1,
35
"forms": 1,
36
"addons": 1,
37
"prefs": 2,
38
"addresses": 1,
39
"creditcards": 1,
40
]
41
42
// Names of collections which will appear as declined in a default
43
// meta/global produced locally.
44
private let DefaultDeclined: [String] = [String]()
45
46
public func computeNewEngines(_ engineConfiguration: EngineConfiguration, enginesEnablements: [String: Bool]?) -> (engines: [String: EngineMeta], declined: [String]) {
47
var enabled: Set<String> = Set(engineConfiguration.enabled)
48
var declined: Set<String> = Set(engineConfiguration.declined)
49
var engines: [String: EngineMeta] = [:]
50
51
if let enginesEnablements = enginesEnablements {
52
let enabledLocally = Set(enginesEnablements.filter { $0.value }.map { $0.key })
53
let declinedLocally = Set(enginesEnablements.filter { !$0.value }.map { $0.key })
54
enabled.subtract(declinedLocally)
55
declined.subtract(enabledLocally)
56
enabled.formUnion(enabledLocally)
57
declined.formUnion(declinedLocally)
58
}
59
60
for engine in enabled {
61
// We take this device's version, or, if we don't know the correct version, 0. Another client should recognize
62
// the engine, see an old version, wipe and start again.
63
// TODO: this client does not yet do this wipe-and-update itself!
64
let version = DefaultEngines[engine] ?? 0
65
engines[engine] = EngineMeta(version: version, syncID: Bytes.generateGUID())
66
}
67
68
return (engines: engines, declined: Array(declined))
69
}
70
71
// public for testing.
72
public func createMetaGlobalWithEngineConfiguration(_ engineConfiguration: EngineConfiguration, enginesEnablements: [String: Bool]?) -> MetaGlobal {
73
let (engines, declined) = computeNewEngines(engineConfiguration, enginesEnablements: enginesEnablements)
74
return MetaGlobal(syncID: Bytes.generateGUID(), storageVersion: StorageVersionCurrent, engines: engines, declined: declined)
75
}
76
77
public func createMetaGlobal(enginesEnablements: [String: Bool]?) -> MetaGlobal {
78
let engineConfiguration = EngineConfiguration(enabled: Array(DefaultEngines.keys), declined: DefaultDeclined)
79
return createMetaGlobalWithEngineConfiguration(engineConfiguration, enginesEnablements: enginesEnablements)
80
}
81
82
public typealias TokenSource = () -> Deferred<Maybe<TokenServerToken>>
83
public typealias ReadyDeferred = Deferred<Maybe<Ready>>
84
85
// See docs in docs/sync.md.
86
87
// You might be wondering why this doesn't have a Sync15StorageClient like FxALoginStateMachine
88
// does. Well, such a client is pinned to a particular server, and this state machine must
89
// acknowledge that a Sync client occasionally must migrate between two servers, preserving
90
// some state from the last.
91
// The resultant 'Ready' will be able to provide a suitably initialized storage client.
92
open class SyncStateMachine {
93
// The keys are used as a set, to prevent cycles in the state machine.
94
var stateLabelsSeen = [SyncStateLabel: Bool]()
95
var stateLabelSequence = [SyncStateLabel]()
96
97
let stateLabelsAllowed: Set<SyncStateLabel>
98
99
let scratchpadPrefs: Prefs
100
101
/// Use this set of states to constrain the state machine to attempt the barest
102
/// minimum to get to Ready. This is suitable for extension uses. If it is not possible,
103
/// then no destructive or expensive actions are taken (e.g. total HTTP requests,
104
/// duration, records processed, database writes, fsyncs, blanking any local collections)
105
public static let OptimisticStates = Set(SyncStateLabel.optimisticValues)
106
107
/// The default set of states that the state machine is allowed to use.
108
public static let AllStates = Set(SyncStateLabel.allValues)
109
110
public init(prefs: Prefs, allowingStates labels: Set<SyncStateLabel> = SyncStateMachine.AllStates) {
111
self.scratchpadPrefs = prefs.branch("scratchpad")
112
self.stateLabelsAllowed = labels
113
}
114
115
open class func clearStateFromPrefs(_ prefs: Prefs) {
116
log.debug("Clearing all Sync prefs.")
117
Scratchpad.clearFromPrefs(prefs.branch("scratchpad")) // XXX this is convoluted.
118
prefs.clearAll()
119
}
120
121
fileprivate func advanceFromState(_ state: SyncState) -> ReadyDeferred {
122
log.info("advanceFromState: \(state.label)")
123
124
// Record visibility before taking any action.
125
let labelAlreadySeen = self.stateLabelsSeen.updateValue(true, forKey: state.label) != nil
126
stateLabelSequence.append(state.label)
127
128
if let ready = state as? Ready {
129
// Sweet, we made it!
130
return deferMaybe(ready)
131
}
132
133
// Cycles are not necessarily a problem, but seeing the same (recoverable) error condition is a problem.
134
if state is RecoverableSyncState && labelAlreadySeen {
135
return deferMaybe(StateMachineCycleError())
136
}
137
138
guard stateLabelsAllowed.contains(state.label) else {
139
return deferMaybe(DisallowedStateError(state.label, allowedStates: stateLabelsAllowed))
140
}
141
142
return state.advance() >>== self.advanceFromState
143
}
144
145
open func toReady(_ authState: SyncAuthState) -> ReadyDeferred {
146
let token = authState.token(Date.now(), canBeExpired: false)
147
return chainDeferred(token, f: { (token, kSync) in
148
log.debug("Got token from auth state.")
149
if Logger.logPII {
150
log.debug("Server is \(token.api_endpoint).")
151
}
152
let prior = Scratchpad.restoreFromPrefs(self.scratchpadPrefs, syncKeyBundle: KeyBundle.fromKSync(kSync))
153
if prior == nil {
154
log.info("No persisted Sync state. Starting over.")
155
}
156
var scratchpad = prior ?? Scratchpad(b: KeyBundle.fromKSync(kSync), persistingTo: self.scratchpadPrefs)
157
158
// Take the scratchpad and add the fxaDeviceId from the state, and hashedUID from the token
159
let b = Scratchpad.Builder(p: scratchpad)
160
if let deviceID = authState.deviceID {
161
b.fxaDeviceId = deviceID
162
} else {
163
// Either deviceRegistration hasn't occurred yet (our bug) or
164
// FxA has given us an UnknownDevice error.
165
log.warning("Device registration has not taken place before sync.")
166
}
167
b.hashedUID = token.hashedFxAUID
168
169
if let enginesEnablements = authState.enginesEnablements,
170
!enginesEnablements.isEmpty {
171
b.enginesEnablements = enginesEnablements
172
}
173
174
if let clientName = authState.clientName {
175
b.clientName = clientName
176
}
177
178
// Detect if we've changed anything in our client record from the last time we synced…
179
let ourClientUnchanged = (b.fxaDeviceId == scratchpad.fxaDeviceId)
180
181
// …and if so, trigger a reset of clients.
182
if !ourClientUnchanged {
183
b.localCommands.insert(LocalCommand.resetEngine(engine: "clients"))
184
}
185
186
scratchpad = b.build()
187
188
log.info("Advancing to InitialWithLiveToken.")
189
let state = InitialWithLiveToken(scratchpad: scratchpad, token: token)
190
191
// Start with fresh visibility data.
192
self.stateLabelsSeen = [:]
193
self.stateLabelSequence = []
194
195
return self.advanceFromState(state)
196
})
197
}
198
}
199
200
public enum SyncStateLabel: String {
201
case Stub = "STUB" // For 'abstract' base classes.
202
203
case InitialWithExpiredToken = "initialWithExpiredToken"
204
case InitialWithExpiredTokenAndInfo = "initialWithExpiredTokenAndInfo"
205
case InitialWithLiveToken = "initialWithLiveToken"
206
case InitialWithLiveTokenAndInfo = "initialWithLiveTokenAndInfo"
207
case ResolveMetaGlobalVersion = "resolveMetaGlobalVersion"
208
case ResolveMetaGlobalContent = "resolveMetaGlobalContent"
209
case NeedsFreshMetaGlobal = "needsFreshMetaGlobal"
210
case NewMetaGlobal = "newMetaGlobal"
211
case HasMetaGlobal = "hasMetaGlobal"
212
case NeedsFreshCryptoKeys = "needsFreshCryptoKeys"
213
case HasFreshCryptoKeys = "hasFreshCryptoKeys"
214
case Ready = "ready"
215
case FreshStartRequired = "freshStartRequired" // Go around again... once only, perhaps.
216
case ServerConfigurationRequired = "serverConfigurationRequired"
217
218
case ChangedServer = "changedServer"
219
case MissingMetaGlobal = "missingMetaGlobal"
220
case MissingCryptoKeys = "missingCryptoKeys"
221
case MalformedCryptoKeys = "malformedCryptoKeys"
222
case SyncIDChanged = "syncIDChanged"
223
case RemoteUpgradeRequired = "remoteUpgradeRequired"
224
case ClientUpgradeRequired = "clientUpgradeRequired"
225
226
static let allValues: [SyncStateLabel] = [
227
InitialWithExpiredToken,
228
InitialWithExpiredTokenAndInfo,
229
InitialWithLiveToken,
230
InitialWithLiveTokenAndInfo,
231
NeedsFreshMetaGlobal,
232
ResolveMetaGlobalVersion,
233
ResolveMetaGlobalContent,
234
NewMetaGlobal,
235
HasMetaGlobal,
236
NeedsFreshCryptoKeys,
237
HasFreshCryptoKeys,
238
Ready,
239
240
FreshStartRequired,
241
ServerConfigurationRequired,
242
243
ChangedServer,
244
MissingMetaGlobal,
245
MissingCryptoKeys,
246
MalformedCryptoKeys,
247
SyncIDChanged,
248
RemoteUpgradeRequired,
249
ClientUpgradeRequired,
250
]
251
252
// This is the list of states needed to get to Ready, or failing.
253
// This is useful in circumstances where it is important to conserve time and/or battery, and failure
254
// to timely sync is acceptable.
255
static let optimisticValues: [SyncStateLabel] = [
256
InitialWithLiveToken,
257
InitialWithLiveTokenAndInfo,
258
HasMetaGlobal,
259
HasFreshCryptoKeys,
260
Ready,
261
]
262
}
263
264
/**
265
* States in this state machine all implement SyncState.
266
*
267
* States are either successful main-flow states, or (recoverable) error states.
268
* Errors that aren't recoverable are simply errors.
269
* Main-flow states flow one to one.
270
*
271
* (Terminal failure states might be introduced at some point.)
272
*
273
* Multiple error states (but typically only one) can arise from each main state transition.
274
* For example, parsing meta/global can result in a number of different non-routine situations.
275
*
276
* For these reasons, and the lack of useful ADTs in Swift, we model the main flow as
277
* the success branch of a Result, and the recovery flows as a part of the failure branch.
278
*
279
* We could just as easily use a ternary Either-style operator, but thanks to Swift's
280
* optional-cast-let it's no saving to do so.
281
*
282
* Because of the lack of type system support, all RecoverableSyncStates must have the same
283
* signature. That signature implies a possibly multi-state transition; individual states
284
* will have richer type signatures.
285
*/
286
public protocol SyncState {
287
var label: SyncStateLabel { get }
288
289
func advance() -> Deferred<Maybe<SyncState>>
290
}
291
292
/*
293
* Base classes to avoid repeating initializers all over the place.
294
*/
295
open class BaseSyncState: SyncState {
296
open var label: SyncStateLabel { return SyncStateLabel.Stub }
297
298
public let client: Sync15StorageClient!
299
let token: TokenServerToken // Maybe expired.
300
var scratchpad: Scratchpad
301
302
// TODO: 304 for i/c.
303
open func getInfoCollections() -> Deferred<Maybe<InfoCollections>> {
304
return chain(self.client.getInfoCollections(), f: {
305
return $0.value
306
})
307
}
308
309
public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken) {
310
self.scratchpad = scratchpad
311
self.token = token
312
self.client = client
313
log.info("Inited \(self.label.rawValue)")
314
}
315
316
open func synchronizer<T: Synchronizer>(_ synchronizerClass: T.Type, delegate: SyncDelegate, prefs: Prefs, why: SyncReason) -> T {
317
return T(scratchpad: self.scratchpad, delegate: delegate, basePrefs: prefs, why: why)
318
}
319
320
// This isn't a convenience initializer 'cos subclasses can't call convenience initializers.
321
public init(scratchpad: Scratchpad, token: TokenServerToken) {
322
let workQueue = DispatchQueue.global()
323
let resultQueue = DispatchQueue.main
324
let backoff = scratchpad.backoffStorage
325
let client = Sync15StorageClient(token: token, workQueue: workQueue, resultQueue: resultQueue, backoff: backoff)
326
self.scratchpad = scratchpad
327
self.token = token
328
self.client = client
329
log.info("Inited \(self.label.rawValue)")
330
}
331
332
open func advance() -> Deferred<Maybe<SyncState>> {
333
return deferMaybe(StubStateError())
334
}
335
}
336
337
open class BaseSyncStateWithInfo: BaseSyncState {
338
public let info: InfoCollections
339
340
init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) {
341
self.info = info
342
super.init(client: client, scratchpad: scratchpad, token: token)
343
}
344
345
init(scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) {
346
self.info = info
347
super.init(scratchpad: scratchpad, token: token)
348
}
349
}
350
351
/*
352
* Error types.
353
*/
354
public protocol SyncError: MaybeErrorType, SyncPingFailureFormattable {}
355
356
extension SyncError {
357
public var failureReasonName: SyncPingFailureReasonName {
358
return .unexpectedError
359
}
360
}
361
362
open class UnknownError: SyncError {
363
open var description: String {
364
return "Unknown error."
365
}
366
}
367
368
open class StateMachineCycleError: SyncError {
369
open var description: String {
370
return "The Sync state machine encountered a cycle. This is a coding error."
371
}
372
}
373
374
open class CouldNotFetchMetaGlobalError: SyncError {
375
open var description: String {
376
return "Could not fetch meta/global."
377
}
378
}
379
380
open class CouldNotFetchKeysError: SyncError {
381
open var description: String {
382
return "Could not fetch crypto/keys."
383
}
384
}
385
386
open class StubStateError: SyncError {
387
open var description: String {
388
return "Unexpectedly reached a stub state. This is a coding error."
389
}
390
}
391
392
open class ClientUpgradeRequiredError: SyncError {
393
let targetStorageVersion: Int
394
395
public init(target: Int) {
396
self.targetStorageVersion = target
397
}
398
399
open var description: String {
400
return "Client upgrade required to work with storage version \(self.targetStorageVersion)."
401
}
402
}
403
404
open class InvalidKeysError: SyncError {
405
let keys: Keys
406
407
public init(_ keys: Keys) {
408
self.keys = keys
409
}
410
411
open var description: String {
412
return "Downloaded crypto/keys, but couldn't parse them."
413
}
414
}
415
416
open class DisallowedStateError: SyncError {
417
let state: SyncStateLabel
418
let allowedStates: Set<SyncStateLabel>
419
420
public init(_ state: SyncStateLabel, allowedStates: Set<SyncStateLabel>) {
421
self.state = state
422
self.allowedStates = allowedStates
423
}
424
425
open var description: String {
426
return "Sync state machine reached \(String(describing: state)) state, which is disallowed. Legal states are: \(String(describing: allowedStates))"
427
}
428
}
429
430
/**
431
* Error states. These are errors that can be recovered from by taking actions. We use RecoverableSyncState as a
432
* sentinel: if we see the same recoverable state twice, we bail out and complain that we've seen a cycle. (Seeing
433
* some states -- principally initial states -- twice is fine.)
434
*/
435
436
public protocol RecoverableSyncState: SyncState {
437
}
438
439
/**
440
* Recovery: discard our local timestamps and sync states; discard caches.
441
* Be prepared to handle a conflict between our selected engines and the new
442
* server's meta/global; if an engine is selected locally but not declined
443
* remotely, then we'll need to upload a new meta/global and sync that engine.
444
*/
445
open class ChangedServerError: RecoverableSyncState {
446
open var label: SyncStateLabel { return SyncStateLabel.ChangedServer }
447
448
let newToken: TokenServerToken
449
let newScratchpad: Scratchpad
450
451
public init(scratchpad: Scratchpad, token: TokenServerToken) {
452
self.newToken = token
453
self.newScratchpad = Scratchpad(b: scratchpad.syncKeyBundle, persistingTo: scratchpad.prefs)
454
}
455
456
open func advance() -> Deferred<Maybe<SyncState>> {
457
// TODO: mutate local storage to allow for a fresh start.
458
let state = InitialWithLiveToken(scratchpad: newScratchpad.checkpoint(), token: newToken)
459
return deferMaybe(state)
460
}
461
}
462
463
/**
464
* Recovery: same as for changed server, but no need to upload a new meta/global.
465
*/
466
open class SyncIDChangedError: RecoverableSyncState {
467
open var label: SyncStateLabel { return SyncStateLabel.SyncIDChanged }
468
469
fileprivate let previousState: BaseSyncStateWithInfo
470
fileprivate let newMetaGlobal: Fetched<MetaGlobal>
471
472
public init(previousState: BaseSyncStateWithInfo, newMetaGlobal: Fetched<MetaGlobal>) {
473
self.previousState = previousState
474
self.newMetaGlobal = newMetaGlobal
475
}
476
477
open func advance() -> Deferred<Maybe<SyncState>> {
478
// TODO: mutate local storage to allow for a fresh start.
479
let s = self.previousState.scratchpad.evolve().setGlobal(self.newMetaGlobal).setKeys(nil).build().checkpoint()
480
let state = HasMetaGlobal(client: self.previousState.client, scratchpad: s, token: self.previousState.token, info: self.previousState.info)
481
return deferMaybe(state)
482
}
483
}
484
485
/**
486
* Recovery: configure the server.
487
*/
488
open class ServerConfigurationRequiredError: RecoverableSyncState {
489
open var label: SyncStateLabel { return SyncStateLabel.ServerConfigurationRequired }
490
491
fileprivate let previousState: BaseSyncStateWithInfo
492
493
public init(previousState: BaseSyncStateWithInfo) {
494
self.previousState = previousState
495
}
496
497
open func advance() -> Deferred<Maybe<SyncState>> {
498
let client = self.previousState.client!
499
let oldScratchpad = self.previousState.scratchpad
500
let enginesEnablements = oldScratchpad.enginesEnablements
501
let s = oldScratchpad.evolve()
502
.setGlobal(nil)
503
.addLocalCommandsFromKeys(nil)
504
.setKeys(nil)
505
.clearEnginesEnablements()
506
.build().checkpoint()
507
// Upload a new meta/global ...
508
let metaGlobal: MetaGlobal
509
if let oldEngineConfiguration = s.engineConfiguration {
510
metaGlobal = createMetaGlobalWithEngineConfiguration(oldEngineConfiguration, enginesEnablements: enginesEnablements)
511
} else {
512
metaGlobal = createMetaGlobal(enginesEnablements: s.enginesEnablements)
513
}
514
return client.uploadMetaGlobal(metaGlobal, ifUnmodifiedSince: nil)
515
// ... and a new crypto/keys.
516
>>> { return client.uploadCryptoKeys(Keys.random(), withSyncKeyBundle: s.syncKeyBundle, ifUnmodifiedSince: nil) }
517
>>> { return deferMaybe(InitialWithLiveToken(client: client, scratchpad: s, token: self.previousState.token)) }
518
}
519
}
520
521
/**
522
* Recovery: wipe the server (perhaps unnecessarily) and proceed to configure the server.
523
*/
524
open class FreshStartRequiredError: RecoverableSyncState {
525
open var label: SyncStateLabel { return SyncStateLabel.FreshStartRequired }
526
527
fileprivate let previousState: BaseSyncStateWithInfo
528
529
public init(previousState: BaseSyncStateWithInfo) {
530
self.previousState = previousState
531
}
532
533
open func advance() -> Deferred<Maybe<SyncState>> {
534
let client = self.previousState.client!
535
return client.wipeStorage()
536
>>> { return deferMaybe(ServerConfigurationRequiredError(previousState: self.previousState)) }
537
}
538
}
539
540
open class MissingMetaGlobalError: RecoverableSyncState {
541
open var label: SyncStateLabel { return SyncStateLabel.MissingMetaGlobal }
542
543
fileprivate let previousState: BaseSyncStateWithInfo
544
545
public init(previousState: BaseSyncStateWithInfo) {
546
self.previousState = previousState
547
}
548
549
open func advance() -> Deferred<Maybe<SyncState>> {
550
return deferMaybe(FreshStartRequiredError(previousState: self.previousState))
551
}
552
}
553
554
open class MissingCryptoKeysError: RecoverableSyncState {
555
open var label: SyncStateLabel { return SyncStateLabel.MissingCryptoKeys }
556
557
fileprivate let previousState: BaseSyncStateWithInfo
558
559
public init(previousState: BaseSyncStateWithInfo) {
560
self.previousState = previousState
561
}
562
563
open func advance() -> Deferred<Maybe<SyncState>> {
564
return deferMaybe(FreshStartRequiredError(previousState: self.previousState))
565
}
566
}
567
568
open class RemoteUpgradeRequired: RecoverableSyncState {
569
open var label: SyncStateLabel { return SyncStateLabel.RemoteUpgradeRequired }
570
571
fileprivate let previousState: BaseSyncStateWithInfo
572
573
public init(previousState: BaseSyncStateWithInfo) {
574
self.previousState = previousState
575
}
576
577
open func advance() -> Deferred<Maybe<SyncState>> {
578
return deferMaybe(FreshStartRequiredError(previousState: self.previousState))
579
}
580
}
581
582
open class ClientUpgradeRequired: RecoverableSyncState {
583
open var label: SyncStateLabel { return SyncStateLabel.ClientUpgradeRequired }
584
585
fileprivate let previousState: BaseSyncStateWithInfo
586
let targetStorageVersion: Int
587
588
public init(previousState: BaseSyncStateWithInfo, target: Int) {
589
self.previousState = previousState
590
self.targetStorageVersion = target
591
}
592
593
open func advance() -> Deferred<Maybe<SyncState>> {
594
return deferMaybe(ClientUpgradeRequiredError(target: self.targetStorageVersion))
595
}
596
}
597
598
/*
599
* Non-error states.
600
*/
601
602
open class InitialWithLiveToken: BaseSyncState {
603
open override var label: SyncStateLabel { return SyncStateLabel.InitialWithLiveToken }
604
605
// This looks totally redundant, but try taking it out, I dare you.
606
public override init(scratchpad: Scratchpad, token: TokenServerToken) {
607
super.init(scratchpad: scratchpad, token: token)
608
}
609
610
// This looks totally redundant, but try taking it out, I dare you.
611
public override init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken) {
612
super.init(client: client, scratchpad: scratchpad, token: token)
613
}
614
615
func advanceWithInfo(_ info: InfoCollections) -> SyncState {
616
return InitialWithLiveTokenAndInfo(scratchpad: self.scratchpad, token: self.token, info: info)
617
}
618
619
override open func advance() -> Deferred<Maybe<SyncState>> {
620
return chain(getInfoCollections(), f: self.advanceWithInfo)
621
}
622
}
623
624
/**
625
* Each time we fetch a new meta/global, we need to reconcile it with our
626
* current state.
627
*
628
* It might be identical to our current meta/global, in which case we can short-circuit.
629
*
630
* We might have no previous meta/global at all, in which case this state
631
* simply configures local storage to be ready to sync according to the
632
* supplied meta/global. (Not necessarily datatype elections: those will be per-device.)
633
*
634
* Or it might be different. In this case the previous m/g and our local user preferences
635
* are compared to the new, resulting in some actions and a final state.
636
*
637
* This states are similar in purpose to GlobalSession.processMetaGlobal in Android Sync.
638
*/
639
640
open class ResolveMetaGlobalVersion: BaseSyncStateWithInfo {
641
let fetched: Fetched<MetaGlobal>
642
643
init(fetched: Fetched<MetaGlobal>, client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) {
644
self.fetched = fetched
645
super.init(client: client, scratchpad: scratchpad, token: token, info: info)
646
}
647
open override var label: SyncStateLabel { return SyncStateLabel.ResolveMetaGlobalVersion }
648
649
class func fromState(_ state: BaseSyncStateWithInfo, fetched: Fetched<MetaGlobal>) -> ResolveMetaGlobalVersion {
650
return ResolveMetaGlobalVersion(fetched: fetched, client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info)
651
}
652
653
override open func advance() -> Deferred<Maybe<SyncState>> {
654
// First: check storage version.
655
let v = fetched.value.storageVersion
656
if v > StorageVersionCurrent {
657
// New storage version? Uh-oh. No recovery possible here.
658
log.info("Client upgrade required for storage version \(v)")
659
return deferMaybe(ClientUpgradeRequired(previousState: self, target: v))
660
}
661
662
if v < StorageVersionCurrent {
663
// Old storage version? Uh-oh. Wipe and upload both meta/global and crypto/keys.
664
log.info("Server storage version \(v) is outdated.")
665
return deferMaybe(RemoteUpgradeRequired(previousState: self))
666
}
667
668
return deferMaybe(ResolveMetaGlobalContent.fromState(self, fetched: self.fetched))
669
}
670
}
671
672
open class ResolveMetaGlobalContent: BaseSyncStateWithInfo {
673
let fetched: Fetched<MetaGlobal>
674
675
init(fetched: Fetched<MetaGlobal>, client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) {
676
self.fetched = fetched
677
super.init(client: client, scratchpad: scratchpad, token: token, info: info)
678
}
679
open override var label: SyncStateLabel { return SyncStateLabel.ResolveMetaGlobalContent }
680
681
class func fromState(_ state: BaseSyncStateWithInfo, fetched: Fetched<MetaGlobal>) -> ResolveMetaGlobalContent {
682
return ResolveMetaGlobalContent(fetched: fetched, client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info)
683
}
684
685
override open func advance() -> Deferred<Maybe<SyncState>> {
686
// Check global syncID and contents.
687
if let previous = self.scratchpad.global?.value {
688
// Do checks that only apply when we're coming from a previous meta/global.
689
if previous.syncID != fetched.value.syncID {
690
log.info("Remote global sync ID has changed. Dropping keys and resetting all local collections.")
691
let s = self.scratchpad.freshStartWithGlobal(fetched).checkpoint()
692
return deferMaybe(HasMetaGlobal.fromState(self, scratchpad: s))
693
}
694
695
let b = self.scratchpad.evolve()
696
.setGlobal(fetched) // We always adopt the upstream meta/global record.
697
698
let previousEngines = Set(previous.engines.keys)
699
let remoteEngines = Set(fetched.value.engines.keys)
700
701
for engine in previousEngines.subtracting(remoteEngines) {
702
log.info("Remote meta/global disabled previously enabled engine \(engine).")
703
b.localCommands.insert(.disableEngine(engine: engine))
704
}
705
706
for engine in remoteEngines.subtracting(previousEngines) {
707
log.info("Remote meta/global enabled previously disabled engine \(engine).")
708
b.localCommands.insert(.enableEngine(engine: engine))
709
}
710
711
for engine in remoteEngines.intersection(previousEngines) {
712
let remoteEngine = fetched.value.engines[engine]!
713
let previousEngine = previous.engines[engine]!
714
if previousEngine.syncID != remoteEngine.syncID {
715
log.info("Remote sync ID for \(engine) has changed. Resetting local.")
716
b.localCommands.insert(.resetEngine(engine: engine))
717
}
718
}
719
720
let s = b.build().checkpoint()
721
return deferMaybe(HasMetaGlobal.fromState(self, scratchpad: s))
722
}
723
724
// No previous meta/global. Adopt the new meta/global.
725
let s = self.scratchpad.freshStartWithGlobal(fetched).checkpoint()
726
return deferMaybe(HasMetaGlobal.fromState(self, scratchpad: s))
727
}
728
}
729
730
private func processFailure(_ failure: MaybeErrorType?) -> MaybeErrorType {
731
if let failure = failure as? ServerInBackoffError {
732
log.warning("Server in backoff. Bailing out. \(failure.description)")
733
return failure
734
}
735
736
// TODO: backoff etc. for all of these.
737
if let failure = failure as? ServerError<HTTPURLResponse> {
738
// Be passive.
739
log.error("Server error. Bailing out. \(failure.description)")
740
return failure
741
}
742
743
if let failure = failure as? BadRequestError<HTTPURLResponse> {
744
// Uh oh.
745
log.error("Bad request. Bailing out. \(failure.description)")
746
return failure
747
}
748
749
log.error("Unexpected failure. \(failure?.description ?? "nil")")
750
return failure ?? UnknownError()
751
}
752
753
open class InitialWithLiveTokenAndInfo: BaseSyncStateWithInfo {
754
open override var label: SyncStateLabel { return SyncStateLabel.InitialWithLiveTokenAndInfo }
755
756
// This method basically hops over HasMetaGlobal, because it's not a state
757
// that we expect consumers to know about.
758
override open func advance() -> Deferred<Maybe<SyncState>> {
759
// Either m/g and c/k are in our local cache, and they're up-to-date with i/c,
760
// or we need to fetch them.
761
// Cached and not changed in i/c? Use that.
762
// This check would be inaccurate if any other fields were stored in meta/; this
763
// has been the case in the past, with the Sync 1.1 migration indicator.
764
if let global = self.scratchpad.global {
765
if let metaModified = self.info.modified("meta") {
766
// We check the last time we fetched the record, and that can be
767
// later than the collection timestamp. All we care about here is if the
768
// server might have a newer record.
769
if global.timestamp >= metaModified {
770
log.debug("Cached meta/global fetched at \(global.timestamp), newer than server modified \(metaModified). Using cached meta/global.")
771
// Strictly speaking we can avoid fetching if this condition is not true,
772
// but if meta/ is modified for a different reason -- store timestamps
773
// for the last collection fetch. This will do for now.
774
return deferMaybe(HasMetaGlobal.fromState(self))
775
}
776
log.info("Cached meta/global fetched at \(global.timestamp) older than server modified \(metaModified). Fetching fresh meta/global.")
777
} else {
778
// No known modified time for meta/. That means the server has no meta/global.
779
// Drop our cached value and fall through; we'll try to fetch, fail, and
780
// go through the usual failure flow.
781
log.warning("Local meta/global fetched at \(global.timestamp) found, but no meta collection on server. Dropping cached meta/global.")
782
// If we bail because we've been overly optimistic, then we nil out the current (broken)
783
// meta/global. Next time around, we end up in the "No cached meta/global found" branch.
784
self.scratchpad = self.scratchpad.evolve().setGlobal(nil).setKeys(nil).build().checkpoint()
785
}
786
} else {
787
log.debug("No cached meta/global found. Fetching fresh meta/global.")
788
}
789
790
return deferMaybe(NeedsFreshMetaGlobal.fromState(self))
791
}
792
}
793
794
/*
795
* We've reached NeedsFreshMetaGlobal somehow, but we haven't yet done anything about it
796
* (e.g. fetch a new one with GET /storage/meta/global ).
797
*
798
* If we don't want to hit the network (e.g. from an extension), we should stop if we get to this state.
799
*/
800
open class NeedsFreshMetaGlobal: BaseSyncStateWithInfo {
801
open override var label: SyncStateLabel { return SyncStateLabel.NeedsFreshMetaGlobal }
802
803
class func fromState(_ state: BaseSyncStateWithInfo) -> NeedsFreshMetaGlobal {
804
return NeedsFreshMetaGlobal(client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info)
805
}
806
807
override open func advance() -> Deferred<Maybe<SyncState>> {
808
// Fetch.
809
return self.client.getMetaGlobal().bind { result in
810
if let resp = result.successValue {
811
// We use the server's timestamp, rather than the record's modified field.
812
// Either can be made to work, but the latter has suffered from bugs: see Bug 1210625.
813
let fetched = Fetched(value: resp.value, timestamp: resp.metadata.timestampMilliseconds)
814
return deferMaybe(ResolveMetaGlobalVersion.fromState(self, fetched: fetched))
815
}
816
817
if let _ = result.failureValue as? NotFound<HTTPURLResponse> {
818
// OK, this is easy.
819
// This state is responsible for creating the new m/g, uploading it, and
820
// restarting with a clean scratchpad.
821
return deferMaybe(MissingMetaGlobalError(previousState: self))
822
}
823
824
// Otherwise, we have a failure state. Die on the sword!
825
return deferMaybe(processFailure(result.failureValue))
826
}
827
}
828
}
829
830
open class HasMetaGlobal: BaseSyncStateWithInfo {
831
open override var label: SyncStateLabel { return SyncStateLabel.HasMetaGlobal }
832
833
class func fromState(_ state: BaseSyncStateWithInfo) -> HasMetaGlobal {
834
return HasMetaGlobal(client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info)
835
}
836
837
class func fromState(_ state: BaseSyncStateWithInfo, scratchpad: Scratchpad) -> HasMetaGlobal {
838
return HasMetaGlobal(client: state.client, scratchpad: scratchpad, token: state.token, info: state.info)
839
}
840
841
override open func advance() -> Deferred<Maybe<SyncState>> {
842
// Check if we have enabled/disabled some engines.
843
if let enginesEnablements = self.scratchpad.enginesEnablements,
844
let oldMetaGlobal = self.scratchpad.global {
845
let (engines, declined) = computeNewEngines(oldMetaGlobal.value.engineConfiguration(), enginesEnablements: enginesEnablements)
846
let newMetaGlobal = MetaGlobal(syncID: oldMetaGlobal.value.syncID, storageVersion: oldMetaGlobal.value.storageVersion, engines: engines, declined: declined)
847
return self.client.uploadMetaGlobal(newMetaGlobal, ifUnmodifiedSince: oldMetaGlobal.timestamp) >>> {
848
self.scratchpad = self.scratchpad.evolve().clearEnginesEnablements().build().checkpoint()
849
return deferMaybe(NeedsFreshMetaGlobal.fromState(self))
850
}
851
}
852
853
// Check if crypto/keys is fresh in the cache already.
854
if let keys = self.scratchpad.keys, keys.value.valid {
855
if let cryptoModified = self.info.modified("crypto") {
856
// Both of these are server timestamps. If the record we stored was fetched after the last time the record was modified, as represented by the "crypto" entry in info/collections, and we're fetching from the
857
// same server, then the record must be identical, and we can use it directly. If are ever additional records in the crypto collection, this will fetch keys too frequently. In that case, we should use X-I-U-S and expect some 304 responses.
858
if keys.timestamp >= cryptoModified {
859
log.debug("Cached keys fetched at \(keys.timestamp), newer than server modified \(cryptoModified). Using cached keys.")
860
return deferMaybe(HasFreshCryptoKeys.fromState(self, scratchpad: self.scratchpad, collectionKeys: keys.value))
861
}
862
863
// The server timestamp is newer, so there might be new keys.
864
// Re-fetch keys and check to see if the actual contents differ.
865
// If the keys are the same, we can ignore this change. If they differ,
866
// we need to re-sync any collection whose keys just changed.
867
log.info("Cached keys fetched at \(keys.timestamp) older than server modified \(cryptoModified). Fetching fresh keys.")
868
return deferMaybe(NeedsFreshCryptoKeys.fromState(self, scratchpad: self.scratchpad, staleCollectionKeys: keys.value))
869
} else {
870
// No known modified time for crypto/. That likely means the server has no keys.
871
// Drop our cached value and fall through; we'll try to fetch, fail, and
872
// go through the usual failure flow.
873
log.warning("Local keys fetched at \(keys.timestamp) found, but no crypto collection on server. Dropping cached keys.")
874
self.scratchpad = self.scratchpad.evolve().setKeys(nil).build().checkpoint()
875
}
876
} else {
877
log.debug("No cached keys found. Fetching fresh keys.")
878
}
879
880
return deferMaybe(NeedsFreshCryptoKeys.fromState(self, scratchpad: self.scratchpad, staleCollectionKeys: nil))
881
}
882
}
883
884
open class NeedsFreshCryptoKeys: BaseSyncStateWithInfo {
885
open override var label: SyncStateLabel { return SyncStateLabel.NeedsFreshCryptoKeys }
886
let staleCollectionKeys: Keys?
887
888
class func fromState(_ state: BaseSyncStateWithInfo, scratchpad: Scratchpad, staleCollectionKeys: Keys?) -> NeedsFreshCryptoKeys {
889
return NeedsFreshCryptoKeys(client: state.client, scratchpad: scratchpad, token: state.token, info: state.info, keys: staleCollectionKeys)
890
}
891
892
public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections, keys: Keys?) {
893
self.staleCollectionKeys = keys
894
super.init(client: client, scratchpad: scratchpad, token: token, info: info)
895
}
896
897
override open func advance() -> Deferred<Maybe<SyncState>> {
898
// Fetch crypto/keys.
899
return self.client.getCryptoKeys(self.scratchpad.syncKeyBundle, ifUnmodifiedSince: nil).bind { result in
900
if let resp = result.successValue {
901
let collectionKeys = Keys(payload: resp.value.payload)
902
if !collectionKeys.valid {
903
log.error("Unexpectedly invalid crypto/keys during a successful fetch.")
904
return Deferred(value: Maybe(failure: InvalidKeysError(collectionKeys)))
905
}
906
907
let fetched = Fetched(value: collectionKeys, timestamp: resp.metadata.timestampMilliseconds)
908
let s = self.scratchpad.evolve()
909
.addLocalCommandsFromKeys(fetched)
910
.setKeys(fetched)
911
.build().checkpoint()
912
return deferMaybe(HasFreshCryptoKeys.fromState(self, scratchpad: s, collectionKeys: collectionKeys))
913
}
914
915
if let _ = result.failureValue as? NotFound<HTTPURLResponse> {
916
// No crypto/keys? We can handle this. Wipe and upload both meta/global and crypto/keys.
917
return deferMaybe(MissingCryptoKeysError(previousState: self))
918
}
919
920
// Otherwise, we have a failure state.
921
return deferMaybe(processFailure(result.failureValue))
922
}
923
}
924
}
925
926
open class HasFreshCryptoKeys: BaseSyncStateWithInfo {
927
open override var label: SyncStateLabel { return SyncStateLabel.HasFreshCryptoKeys }
928
let collectionKeys: Keys
929
930
class func fromState(_ state: BaseSyncStateWithInfo, scratchpad: Scratchpad, collectionKeys: Keys) -> HasFreshCryptoKeys {
931
return HasFreshCryptoKeys(client: state.client, scratchpad: scratchpad, token: state.token, info: state.info, keys: collectionKeys)
932
}
933
934
public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections, keys: Keys) {
935
self.collectionKeys = keys
936
super.init(client: client, scratchpad: scratchpad, token: token, info: info)
937
}
938
939
override open func advance() -> Deferred<Maybe<SyncState>> {
940
return deferMaybe(Ready(client: self.client, scratchpad: self.scratchpad, token: self.token, info: self.info, keys: self.collectionKeys))
941
}
942
}
943
944
public protocol EngineStateChanges {
945
func collectionsThatNeedLocalReset() -> [String]
946
func enginesEnabled() -> [String]
947
func enginesDisabled() -> [String]
948
func clearLocalCommands()
949
}
950
951
open class Ready: BaseSyncStateWithInfo {
952
open override var label: SyncStateLabel { return SyncStateLabel.Ready }
953
let collectionKeys: Keys
954
955
public var hashedFxADeviceID: String {
956
return (scratchpad.fxaDeviceId + token.hashedFxAUID).sha256.hexEncodedString
957
}
958
959
public var engineConfiguration: EngineConfiguration? {
960
return scratchpad.engineConfiguration
961
}
962
963
public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections, keys: Keys) {
964
self.collectionKeys = keys
965
super.init(client: client, scratchpad: scratchpad, token: token, info: info)
966
}
967
}
968
969
extension Ready: EngineStateChanges {
970
public func collectionsThatNeedLocalReset() -> [String] {
971
var needReset: Set<String> = Set()
972
for command in self.scratchpad.localCommands {
973
switch command {
974
case let .resetAllEngines(except: except):
975
needReset.formUnion(Set(LocalEngines).subtracting(except))
976
case let .resetEngine(engine):
977
needReset.insert(engine)
978
case .enableEngine, .disableEngine:
979
break
980
}
981
}
982
return Array(needReset).sorted()
983
}
984
985
public func enginesEnabled() -> [String] {
986
var engines: Set<String> = Set()
987
for command in self.scratchpad.localCommands {
988
switch command {
989
case let .enableEngine(engine):
990
engines.insert(engine)
991
default:
992
break
993
}
994
}
995
return Array(engines).sorted()
996
}
997
998
public func enginesDisabled() -> [String] {
999
var engines: Set<String> = Set()
1000
for command in self.scratchpad.localCommands {
1001
switch command {
1002
case let .disableEngine(engine):
1003
engines.insert(engine)
1004
default:
1005
break
1006
}
1007
}
1008
return Array(engines).sorted()
1009
}
1010
1011
public func clearLocalCommands() {
1012
self.scratchpad = self.scratchpad.evolve().clearLocalCommands().build().checkpoint()
1013
}
1014
}