mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-15 08:53:07 +09:00
294 lines
11 KiB
Swift
294 lines
11 KiB
Swift
/* 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 Deferred
|
|
import Shared
|
|
import SwiftyJSON
|
|
|
|
let PendingAccountDisconnectedKey = "PendingAccountDisconnect"
|
|
|
|
/// This class provides handles push messages from FxA.
|
|
/// For reference, the [message schema][0] and [Android implementation][1] are both useful resources.
|
|
/// [0]: https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json#L26
|
|
/// [1]: https://dxr.mozilla.org/mozilla-central/source/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
|
|
/// The main entry points are `handle` methods, to accept the raw APNS `userInfo` and then to process the resulting JSON.
|
|
class FxAPushMessageHandler {
|
|
let profile: Profile
|
|
|
|
init(with profile: Profile) {
|
|
self.profile = profile
|
|
}
|
|
}
|
|
|
|
extension FxAPushMessageHandler {
|
|
/// Accepts the raw Push message from Autopush.
|
|
/// This method then decrypts it according to the content-encoding (aes128gcm or aesgcm)
|
|
/// and then effects changes on the logged in account.
|
|
@discardableResult func handle(userInfo: [AnyHashable: Any]) -> PushMessageResult {
|
|
guard let subscription = profile.getAccount()?.pushRegistration?.defaultSubscription else {
|
|
return deferMaybe(PushMessageError.notDecrypted)
|
|
}
|
|
|
|
guard let encoding = userInfo["con"] as? String, // content-encoding
|
|
let payload = userInfo["body"] as? String else {
|
|
return handleVerification()
|
|
}
|
|
// ver == endpointURL path, chid == channel id, aps == alert text and content_available.
|
|
|
|
let plaintext: String?
|
|
if let cryptoKeyHeader = userInfo["cryptokey"] as? String, // crypto-key
|
|
let encryptionHeader = userInfo["enc"] as? String, // encryption
|
|
encoding == "aesgcm" {
|
|
plaintext = subscription.aesgcm(payload: payload, encryptionHeader: encryptionHeader, cryptoHeader: cryptoKeyHeader)
|
|
} else if encoding == "aes128gcm" {
|
|
plaintext = subscription.aes128gcm(payload: payload)
|
|
} else {
|
|
plaintext = nil
|
|
}
|
|
|
|
guard let string = plaintext else {
|
|
return deferMaybe(PushMessageError.notDecrypted)
|
|
}
|
|
|
|
return handle(plaintext: string)
|
|
}
|
|
|
|
func handle(plaintext: String) -> PushMessageResult {
|
|
return handle(message: JSON(parseJSON: plaintext))
|
|
}
|
|
|
|
/// The main entry point to the handler for decrypted messages.
|
|
func handle(message json: JSON) -> PushMessageResult {
|
|
if !json.isDictionary() || json.isEmpty {
|
|
return handleVerification()
|
|
}
|
|
|
|
let rawValue = json["command"].stringValue
|
|
guard let command = PushMessageType(rawValue: rawValue) else {
|
|
print("Command \(rawValue) received but not recognized")
|
|
return deferMaybe(PushMessageError.messageIncomplete)
|
|
}
|
|
|
|
let result: PushMessageResult
|
|
switch command {
|
|
case .deviceConnected:
|
|
result = handleDeviceConnected(json["data"])
|
|
case .deviceDisconnected:
|
|
result = handleDeviceDisconnected(json["data"])
|
|
case .profileUpdated:
|
|
result = handleProfileUpdated()
|
|
case .passwordChanged:
|
|
result = handlePasswordChanged()
|
|
case .passwordReset:
|
|
result = handlePasswordReset()
|
|
case .collectionChanged:
|
|
result = handleCollectionChanged(json["data"])
|
|
case .accountVerified:
|
|
result = handleVerification()
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
extension FxAPushMessageHandler {
|
|
func handleVerification() -> PushMessageResult {
|
|
// What we'd really like to be able to start syncing immediately we receive this
|
|
// message, but this method is run by the extension, so we can't do it here.
|
|
return deferMaybe(.accountVerified)
|
|
}
|
|
|
|
// This will be executed by the app, not the extension.
|
|
// This isn't guaranteed to be run (when the app is backgrounded, and the user
|
|
// doesn't tap on the notification), but that's okay because:
|
|
// We'll naturally be syncing shortly after startup.
|
|
func postVerification() -> Success {
|
|
if let account = profile.getAccount(),
|
|
let syncManager = profile.syncManager {
|
|
return account.advance().bind { _ in
|
|
return syncManager.syncEverything(why: .didLogin)
|
|
} >>> succeed
|
|
}
|
|
return succeed()
|
|
}
|
|
}
|
|
|
|
/// An extension to handle each of the messages.
|
|
extension FxAPushMessageHandler {
|
|
func handleDeviceConnected(_ data: JSON?) -> PushMessageResult {
|
|
guard let deviceName = data?["deviceName"].string else {
|
|
return messageIncomplete(.deviceConnected)
|
|
}
|
|
let message = PushMessage.deviceConnected(deviceName)
|
|
return deferMaybe(message)
|
|
}
|
|
}
|
|
|
|
extension FxAPushMessageHandler {
|
|
func handleDeviceDisconnected(_ data: JSON?) -> PushMessageResult {
|
|
guard let deviceId = data?["id"].string else {
|
|
return messageIncomplete(.deviceDisconnected)
|
|
}
|
|
|
|
if let ourDeviceId = self.getOurDeviceId(), deviceId == ourDeviceId {
|
|
// We can't disconnect the device from the account until we have
|
|
// access to the application, so we'll handle this properly in the AppDelegate,
|
|
// by calling the FxALoginHelper.applicationDidDisonnect(application).
|
|
profile.prefs.setBool(true, forKey: PendingAccountDisconnectedKey)
|
|
return deferMaybe(PushMessage.thisDeviceDisconnected)
|
|
}
|
|
|
|
guard let profile = self.profile as? BrowserProfile else {
|
|
// We can't look up a name in testing, so this is the same as
|
|
// not knowing about it.
|
|
return deferMaybe(PushMessage.deviceDisconnected(nil))
|
|
}
|
|
|
|
let clients = profile.remoteClientsAndTabs
|
|
let getClient = clients.getClient(fxaDeviceId: deviceId)
|
|
|
|
return getClient >>== { device in
|
|
let message = PushMessage.deviceDisconnected(device?.name)
|
|
if let id = device?.guid {
|
|
return clients.deleteClient(guid: id) >>== { _ in deferMaybe(message) }
|
|
}
|
|
|
|
return deferMaybe(message)
|
|
}
|
|
}
|
|
|
|
fileprivate func getOurDeviceId() -> String? {
|
|
return profile.getAccount()?.deviceRegistration?.id
|
|
}
|
|
}
|
|
|
|
extension FxAPushMessageHandler {
|
|
func handleProfileUpdated() -> PushMessageResult {
|
|
return unimplemented(.profileUpdated)
|
|
}
|
|
}
|
|
|
|
extension FxAPushMessageHandler {
|
|
func handlePasswordChanged() -> PushMessageResult {
|
|
return unimplemented(.passwordChanged)
|
|
}
|
|
}
|
|
|
|
extension FxAPushMessageHandler {
|
|
func handlePasswordReset() -> PushMessageResult {
|
|
return unimplemented(.passwordReset)
|
|
}
|
|
}
|
|
|
|
extension FxAPushMessageHandler {
|
|
func handleCollectionChanged(_ data: JSON?) -> PushMessageResult {
|
|
guard let collections = data?["collections"].arrayObject as? [String] else {
|
|
print("collections_changed received but incomplete: \(data ?? "nil")")
|
|
return deferMaybe(PushMessageError.messageIncomplete)
|
|
}
|
|
// Possible values: "addons", "bookmarks", "history", "forms", "prefs", "tabs", "passwords", "clients"
|
|
|
|
// syncManager will only do a subset; others will be ignored.
|
|
return profile.syncManager.syncNamedCollections(why: .push, names: collections) >>== { deferMaybe(.collectionChanged(collections: collections)) }
|
|
}
|
|
}
|
|
|
|
/// Some utility methods
|
|
fileprivate extension FxAPushMessageHandler {
|
|
func unimplemented(_ messageType: PushMessageType, with param: String? = nil) -> PushMessageResult {
|
|
if let param = param {
|
|
print("\(messageType) message received with parameter = \(param), but unimplemented")
|
|
} else {
|
|
print("\(messageType) message received, but unimplemented")
|
|
}
|
|
return deferMaybe(PushMessageError.unimplemented(messageType))
|
|
}
|
|
|
|
func messageIncomplete(_ messageType: PushMessageType) -> PushMessageResult {
|
|
print("\(messageType) message received, but incomplete")
|
|
return deferMaybe(PushMessageError.messageIncomplete)
|
|
}
|
|
}
|
|
|
|
enum PushMessageType: String {
|
|
case deviceConnected = "fxaccounts:device_connected"
|
|
case deviceDisconnected = "fxaccounts:device_disconnected"
|
|
case profileUpdated = "fxaccounts:profile_updated"
|
|
case passwordChanged = "fxaccounts:password_changed"
|
|
case passwordReset = "fxaccounts:password_reset"
|
|
case collectionChanged = "sync:collection_changed"
|
|
|
|
// This isn't a real message type, just the absence of one.
|
|
case accountVerified = "account_verified"
|
|
}
|
|
|
|
enum PushMessage: Equatable {
|
|
case deviceConnected(String)
|
|
case deviceDisconnected(String?)
|
|
case profileUpdated
|
|
case passwordChanged
|
|
case passwordReset
|
|
case collectionChanged(collections: [String])
|
|
case accountVerified
|
|
|
|
// This is returned when we detect that it is us that has been disconnected.
|
|
case thisDeviceDisconnected
|
|
|
|
var messageType: PushMessageType {
|
|
switch self {
|
|
case .deviceConnected(_):
|
|
return .deviceConnected
|
|
case .deviceDisconnected(_):
|
|
return .deviceDisconnected
|
|
case .thisDeviceDisconnected:
|
|
return .deviceDisconnected
|
|
case .profileUpdated:
|
|
return .profileUpdated
|
|
case .passwordChanged:
|
|
return .passwordChanged
|
|
case .passwordReset:
|
|
return .passwordReset
|
|
case .collectionChanged(collections: _):
|
|
return .collectionChanged
|
|
case .accountVerified:
|
|
return .accountVerified
|
|
}
|
|
}
|
|
|
|
public static func ==(lhs: PushMessage, rhs: PushMessage) -> Bool {
|
|
guard lhs.messageType == rhs.messageType else {
|
|
return false
|
|
}
|
|
|
|
switch (lhs, rhs) {
|
|
case (.deviceConnected(let lName), .deviceConnected(let rName)):
|
|
return lName == rName
|
|
case (.collectionChanged(let lList), .collectionChanged(let rList)):
|
|
return lList == rList
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
typealias PushMessageResult = Deferred<Maybe<PushMessage>>
|
|
|
|
enum PushMessageError: MaybeErrorType {
|
|
case notDecrypted
|
|
case messageIncomplete
|
|
case unimplemented(PushMessageType)
|
|
case timeout
|
|
case accountError
|
|
case noProfile
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .notDecrypted: return "notDecrypted"
|
|
case .messageIncomplete: return "messageIncomplete"
|
|
case .unimplemented(let what): return "unimplemented=\(what)"
|
|
case .timeout: return "timeout"
|
|
case .accountError: return "accountError"
|
|
case .noProfile: return "noProfile"
|
|
}
|
|
}
|
|
}
|