2  * Copyright (c) 2018 Apple Inc. All Rights Reserved.
 
   4  * @APPLE_LICENSE_HEADER_START@
 
   6  * This file contains Original Code and/or Modifications of Original Code
 
   7  * as defined in and that are subject to the Apple Public Source License
 
   8  * Version 2.0 (the 'License'). You may not use this file except in
 
   9  * compliance with the License. Please obtain a copy of the License at
 
  10  * http://www.opensource.apple.com/apsl/ and read it before using this
 
  13  * The Original Code and all software distributed under the License are
 
  14  * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 
  15  * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 
  16  * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 
  17  * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 
  18  * Please see the License for the specific language governing rights and
 
  19  * limitations under the License.
 
  21  * @APPLE_LICENSE_HEADER_END@
 
  25 import CloudKit_Private
 
  27 import CloudKitCodeProtobuf
 
  31 // TODO: merge into CodeConnection
 
  33 let CuttlefishPushTopicBundleIdentifier = "com.apple.security.cuttlefish"
 
  35 struct CKInternalErrorMatcher {
 
  40 // Match a CKError/CKInternalError
 
  41 func ~= (pattern: CKInternalErrorMatcher, value: Error?) -> Bool {
 
  42     guard let value = value else {
 
  45     let error = value as NSError
 
  46     guard let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError else {
 
  49     return error.domain == CKErrorDomain && error.code == pattern.code &&
 
  50         underlyingError.domain == CKInternalErrorDomain && underlyingError.code == pattern.internalCode
 
  53 struct CKErrorMatcher {
 
  58 func ~= (pattern: CKErrorMatcher, value: Error?) -> Bool {
 
  59     guard let value = value else {
 
  62     let error = value as NSError
 
  63     return error.domain == CKErrorDomain && error.code == pattern.code
 
  66 struct NSURLErrorMatcher {
 
  70 // Match an NSURLError
 
  71 func ~= (pattern: NSURLErrorMatcher, value: Error?) -> Bool {
 
  72     guard let value = value else {
 
  75     let error = value as NSError
 
  76     return error.domain == NSURLErrorDomain && error.code == pattern.code
 
  79 public class RetryingInvocable: CloudKitCode.Invocable {
 
  80     private let underlyingInvocable: CloudKitCode.Invocable
 
  82     internal init(retry: CloudKitCode.Invocable) {
 
  83         self.underlyingInvocable = retry
 
  86     public class func retryableError(error: Error?) -> Bool {
 
  88         case NSURLErrorMatcher(code: NSURLErrorTimedOut):
 
  90         case CKErrorMatcher(code: CKError.networkFailure.rawValue):
 
  92         case CKInternalErrorMatcher(code: CKError.serverRejectedRequest.rawValue, internalCode: CKInternalErrorCode.errorInternalServerInternalError.rawValue):
 
  94         case CuttlefishErrorMatcher(code: CuttlefishErrorCode.retryableServerFailure):
 
  96         case CuttlefishErrorMatcher(code: CuttlefishErrorCode.transactionalFailure):
 
 103     public func invoke<RequestType, ResponseType>(function: String,
 
 104                                                   request: RequestType,
 
 105                                                   completion: @escaping (ResponseType?, Error?) -> Void) where RequestType: Message, ResponseType: Message {
 
 107         let deadline = Date(timeInterval: 30, since: now)
 
 108         let delay = TimeInterval(5)
 
 110         self.invokeRetry(function: function,
 
 114                          completion: completion)
 
 117     private func invokeRetry<RequestType: Message, ResponseType: Message>(
 
 119         request: RequestType,
 
 121         minimumDelay: TimeInterval,
 
 122         completion: @escaping (ResponseType?, Error?) -> Void) {
 
 123         self.underlyingInvocable.invoke(function: function,
 
 124                                         request: request) { (response: ResponseType?, error: Error?) in
 
 125                                             if let error = error, RetryingInvocable.retryableError(error: error) {
 
 128                                                 // Check cuttlefish and CKError retry afters.
 
 129                                                 let cuttlefishDelay = CuttlefishRetryAfter(error: error)
 
 130                                                 let ckDelay = CKRetryAfterSecondsForError(error)
 
 131                                                 let delay = max(minimumDelay, cuttlefishDelay, ckDelay)
 
 132                                                 let cutoff = Date(timeInterval: delay, since: now)
 
 134                                                 guard cutoff.compare(deadline) == ComparisonResult.orderedDescending else {
 
 135                                                     Thread.sleep(forTimeInterval: delay)
 
 136                                                     os_log("%{public}@ error: %{public}@ (retrying, now=%{public}@, deadline=%{public}@)", log: tplogDebug,
 
 138                                                            "\(String(describing: error))",
 
 139                                                         "\(String(describing: now))",
 
 140                                                         "\(String(describing: deadline))")
 
 141                                                     self.invokeRetry(function: function,
 
 144                                                                      minimumDelay: minimumDelay,
 
 145                                                                      completion: completion)
 
 149                                             completion(response, error)
 
 154 public class MyCodeConnection: CloudKitCode.Invocable {
 
 155     private let serviceName: String
 
 156     private let container: CKContainer
 
 157     private let databaseScope: CKDatabase.Scope
 
 158     private let local: Bool
 
 159     private let queue: DispatchQueue
 
 161     internal init(service: String, container: CKContainer, databaseScope: CKDatabase.Scope, local: Bool) {
 
 162         self.serviceName = service
 
 163         self.container = container
 
 164         self.databaseScope = databaseScope
 
 166         self.queue = DispatchQueue(label: "MyCodeConnection", qos: .userInitiated)
 
 169     /// Temporary stand-in until xcinverness moves to a swift-grpc plugin.
 
 170     /// Intended to be used by protoc-generated code only
 
 171     public func invoke<RequestType: Message, ResponseType: Message>(
 
 172         function: String, request: RequestType,
 
 173         completion: @escaping (ResponseType?, Error?) -> Void) {
 
 174         // Hack to fool CloudKit, real solution is tracked in <rdar://problem/49086080>
 
 176             let operation = CodeOperation<RequestType, ResponseType>(
 
 177                 service: self.serviceName,
 
 178                 functionName: function,
 
 180                 destinationServer: self.local ? .local : .default)
 
 182             // As each UUID finishes, log it.
 
 183             let requestCompletion = { (requestInfo: CKRequestInfo?) -> Void in
 
 184                 if let requestUUID = requestInfo?.requestUUID {
 
 185                     os_log("ckoperation request finished: %{public}@ %{public}@", log: tplogDebug, function, requestUUID)
 
 188             operation.requestCompletedBlock = requestCompletion
 
 190             let loggingCompletion = { (response: ResponseType?, error: Error?) -> Void in
 
 191                 os_log("%{public}@(%{public}@): %{public}@, error: %{public}@",
 
 194                        "\(String(describing: request))",
 
 195                        "\(String(describing: response))",
 
 196                        "\(String(describing: error))")
 
 197                 completion(response, error)
 
 199             operation.codeOperationCompletionBlock = loggingCompletion
 
 201             /* Same convenience API properties that we specify in CKContainer / CKDatabase */
 
 202             operation.queuePriority = .low
 
 204             // One alternative here would be to not set any QoS and trust the
 
 205             // QoS propagation to do what's right. But there is also some benefit in
 
 206             // just using a lower CPU QoS because it should be hard to measure for the
 
 208             operation.qualityOfService = .userInitiated
 
 210             operation.configuration.discretionaryNetworkBehavior = .nonDiscretionary
 
 211             operation.configuration.automaticallyRetryNetworkFailures = false
 
 212             operation.configuration.isCloudKitSupportOperation = true
 
 213             operation.configuration.setApplicationBundleIdentifierOverride(CuttlefishPushTopicBundleIdentifier)
 
 215             let database = self.container.database(with: self.databaseScope)
 
 217             database.add(operation)
 
 222 protocol ContainerNameToCuttlefishInvocable {
 
 223     func client(container: String) -> CloudKitCode.Invocable
 
 226 class CKCodeCuttlefishInvocableCreator: ContainerNameToCuttlefishInvocable {
 
 227     func client(container: String) -> CloudKitCode.Invocable {
 
 228         // Set up Cuttlefish client connection
 
 229         let ckContainer = CKContainer(identifier: container)
 
 231         // Cuttlefish is using its own push topic.
 
 232         // To register for this push topic, we need to issue CK operations with a specific bundle identifier
 
 233         ckContainer.options.setApplicationBundleIdentifierOverride(CuttlefishPushTopicBundleIdentifier)
 
 235         let ckDatabase = ckContainer.privateCloudDatabase
 
 236         return MyCodeConnection(service: "Cuttlefish", container: ckContainer,
 
 237                                 databaseScope: ckDatabase.databaseScope, local: false)
 
 241 // A collection of Containers.
 
 242 // When a Container of a given name is requested, it is created if it did not already exist.
 
 243 // Methods may be invoked concurrently.
 
 245     private let queue = DispatchQueue(label: "com.apple.security.TrustedPeersHelper.ContainerMap")
 
 247     let invocableCreator: ContainerNameToCuttlefishInvocable
 
 249     init(invocableCreator: ContainerNameToCuttlefishInvocable) {
 
 250         self.invocableCreator = invocableCreator
 
 253     // Only access containers while executing on queue
 
 254     private var containers: [ContainerName: Container] = [:]
 
 256     func findOrCreate(name: ContainerName) throws -> Container {
 
 257         return try queue.sync {
 
 258             if let container = self.containers[name] {
 
 261                 // Set up Core Data stack
 
 262                 let persistentStoreURL = ContainerMap.urlForPersistentStore(name: name)
 
 263                 let description = NSPersistentStoreDescription(url: persistentStoreURL)
 
 265                 // Wrap whatever we're given in a magically-retrying layer
 
 266                 let cuttlefishInvocable = self.invocableCreator.client(container: name.container)
 
 267                 let retryingInvocable = RetryingInvocable(retry: cuttlefishInvocable)
 
 268                 let cuttlefish = CuttlefishAPIAsyncClient(invocable: retryingInvocable)
 
 270                 let container = try Container(name: name, persistentStoreDescription: description,
 
 271                                               cuttlefish: cuttlefish)
 
 272                 self.containers[name] = container
 
 278     static func urlForPersistentStore(name: ContainerName) -> URL {
 
 279         let filename = name.container + "-" + name.context + ".TrustedPeersHelper.db"
 
 280         return SecCopyURLForFileInKeychainDirectory(filename as CFString) as URL
 
 283     // To be called via test only
 
 284     func removeAllContainers() {
 
 286             self.containers.removeAll()
 
 290     func deleteAllPersistentStores() throws {
 
 292             try self.containers.forEach {
 
 293                 try $0.value.deletePersistentStore()