]> git.saurik.com Git - apple/security.git/blob - keychain/TrustedPeersHelper/ContainerMap.swift
Security-59754.80.3.tar.gz
[apple/security.git] / keychain / TrustedPeersHelper / ContainerMap.swift
1 /*
2 * Copyright (c) 2018 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 import CloudKit
25 import CloudKit_Private
26 import CloudKitCode
27 import CloudKitCodeProtobuf
28 import CoreData
29 import Foundation
30
31 // TODO: merge into CodeConnection
32
33 let CuttlefishPushTopicBundleIdentifier = "com.apple.security.cuttlefish"
34
35 struct CKInternalErrorMatcher {
36 let code: Int
37 let internalCode: Int
38 }
39
40 // Match a CKError/CKInternalError
41 func ~= (pattern: CKInternalErrorMatcher, value: Error?) -> Bool {
42 guard let value = value else {
43 return false
44 }
45 let error = value as NSError
46 guard let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError else {
47 return false
48 }
49 return error.domain == CKErrorDomain && error.code == pattern.code &&
50 underlyingError.domain == CKInternalErrorDomain && underlyingError.code == pattern.internalCode
51 }
52
53 struct CKErrorMatcher {
54 let code: Int
55 }
56
57 // Match a CKError
58 func ~= (pattern: CKErrorMatcher, value: Error?) -> Bool {
59 guard let value = value else {
60 return false
61 }
62 let error = value as NSError
63 return error.domain == CKErrorDomain && error.code == pattern.code
64 }
65
66 struct NSURLErrorMatcher {
67 let code: Int
68 }
69
70 // Match an NSURLError
71 func ~= (pattern: NSURLErrorMatcher, value: Error?) -> Bool {
72 guard let value = value else {
73 return false
74 }
75 let error = value as NSError
76 return error.domain == NSURLErrorDomain && error.code == pattern.code
77 }
78
79 public class RetryingInvocable: CloudKitCode.Invocable {
80 private let underlyingInvocable: CloudKitCode.Invocable
81
82 internal init(retry: CloudKitCode.Invocable) {
83 self.underlyingInvocable = retry
84 }
85
86 public class func retryableError(error: Error?) -> Bool {
87 switch error {
88 case NSURLErrorMatcher(code: NSURLErrorTimedOut):
89 return true
90 case CKErrorMatcher(code: CKError.networkFailure.rawValue):
91 return true
92 case CKInternalErrorMatcher(code: CKError.serverRejectedRequest.rawValue, internalCode: CKInternalErrorCode.errorInternalServerInternalError.rawValue):
93 return true
94 case CuttlefishErrorMatcher(code: CuttlefishErrorCode.retryableServerFailure):
95 return true
96 case CuttlefishErrorMatcher(code: CuttlefishErrorCode.transactionalFailure):
97 return true
98 default:
99 return false
100 }
101 }
102
103 public func invoke<RequestType, ResponseType>(function: String,
104 request: RequestType,
105 completion: @escaping (ResponseType?, Error?) -> Void) where RequestType: Message, ResponseType: Message {
106 let now = Date()
107 let deadline = Date(timeInterval: 30, since: now)
108 let delay = TimeInterval(5)
109
110 self.invokeRetry(function: function,
111 request: request,
112 deadline: deadline,
113 minimumDelay: delay,
114 completion: completion)
115 }
116
117 private func invokeRetry<RequestType: Message, ResponseType: Message>(
118 function: String,
119 request: RequestType,
120 deadline: Date,
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) {
126 let now = Date()
127
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)
133
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,
137 function,
138 "\(String(describing: error))",
139 "\(String(describing: now))",
140 "\(String(describing: deadline))")
141 self.invokeRetry(function: function,
142 request: request,
143 deadline: deadline,
144 minimumDelay: minimumDelay,
145 completion: completion)
146 return
147 }
148 }
149 completion(response, error)
150 }
151 }
152 }
153
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
160
161 internal init(service: String, container: CKContainer, databaseScope: CKDatabase.Scope, local: Bool) {
162 self.serviceName = service
163 self.container = container
164 self.databaseScope = databaseScope
165 self.local = local
166 self.queue = DispatchQueue(label: "MyCodeConnection", qos: .userInitiated)
167 }
168
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>
175 self.queue.async {
176 let operation = CodeOperation<RequestType, ResponseType>(
177 service: self.serviceName,
178 functionName: function,
179 request: request,
180 destinationServer: self.local ? .local : .default)
181
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)
186 }
187 }
188 operation.requestCompletedBlock = requestCompletion
189
190 let loggingCompletion = { (response: ResponseType?, error: Error?) -> Void in
191 os_log("%{public}@(%{public}@): %{public}@, error: %{public}@",
192 log: tplogDebug,
193 function,
194 "\(String(describing: request))",
195 "\(String(describing: response))",
196 "\(String(describing: error))")
197 completion(response, error)
198 }
199 operation.codeOperationCompletionBlock = loggingCompletion
200
201 /* Same convenience API properties that we specify in CKContainer / CKDatabase */
202 operation.queuePriority = .low
203
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
207 // casual adopter.
208 operation.qualityOfService = .userInitiated
209
210 operation.configuration.discretionaryNetworkBehavior = .nonDiscretionary
211 operation.configuration.automaticallyRetryNetworkFailures = false
212 operation.configuration.isCloudKitSupportOperation = true
213 operation.configuration.setApplicationBundleIdentifierOverride(CuttlefishPushTopicBundleIdentifier)
214
215 let database = self.container.database(with: self.databaseScope)
216
217 database.add(operation)
218 }
219 }
220 }
221
222 protocol ContainerNameToCuttlefishInvocable {
223 func client(container: String) -> CloudKitCode.Invocable
224 }
225
226 class CKCodeCuttlefishInvocableCreator: ContainerNameToCuttlefishInvocable {
227 func client(container: String) -> CloudKitCode.Invocable {
228 // Set up Cuttlefish client connection
229 let ckContainer = CKContainer(identifier: container)
230
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)
234
235 let ckDatabase = ckContainer.privateCloudDatabase
236 return MyCodeConnection(service: "Cuttlefish", container: ckContainer,
237 databaseScope: ckDatabase.databaseScope, local: false)
238 }
239 }
240
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.
244 class ContainerMap {
245 private let queue = DispatchQueue(label: "com.apple.security.TrustedPeersHelper.ContainerMap")
246
247 let invocableCreator: ContainerNameToCuttlefishInvocable
248
249 init(invocableCreator: ContainerNameToCuttlefishInvocable) {
250 self.invocableCreator = invocableCreator
251 }
252
253 // Only access containers while executing on queue
254 private var containers: [ContainerName: Container] = [:]
255
256 func findOrCreate(name: ContainerName) throws -> Container {
257 return try queue.sync {
258 if let container = self.containers[name] {
259 return container
260 } else {
261 // Set up Core Data stack
262 let persistentStoreURL = ContainerMap.urlForPersistentStore(name: name)
263 let description = NSPersistentStoreDescription(url: persistentStoreURL)
264
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)
269
270 let container = try Container(name: name, persistentStoreDescription: description,
271 cuttlefish: cuttlefish)
272 self.containers[name] = container
273 return container
274 }
275 }
276 }
277
278 static func urlForPersistentStore(name: ContainerName) -> URL {
279 let filename = name.container + "-" + name.context + ".TrustedPeersHelper.db"
280 return SecCopyURLForFileInKeychainDirectory(filename as CFString) as URL
281 }
282
283 // To be called via test only
284 func removeAllContainers() {
285 queue.sync {
286 self.containers.removeAll()
287 }
288 }
289
290 func deleteAllPersistentStores() throws {
291 try queue.sync {
292 try self.containers.forEach {
293 try $0.value.deletePersistentStore()
294 }
295 }
296 }
297 }