]> git.saurik.com Git - apple/security.git/blob - keychain/TrustedPeersHelper/ContainerMap.swift
Security-59306.11.20.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 public class RetryingInvocable: CloudKitCode.Invocable {
36 private let underlyingInvocable: CloudKitCode.Invocable
37
38 internal init(retry: CloudKitCode.Invocable) {
39 self.underlyingInvocable = retry
40 }
41
42 private func retryableError(error: Error?) -> Bool {
43 switch error {
44 case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorTimedOut:
45 return true
46 case let error as NSError where error.domain == CKErrorDomain && error.code == CKError.networkFailure.rawValue:
47 return true
48 case CuttlefishErrorMatcher(code: CuttlefishErrorCode.retryableServerFailure):
49 return true
50 default:
51 return false
52 }
53 }
54
55 public func invoke<RequestType, ResponseType>(function: String,
56 request: RequestType,
57 completion: @escaping (ResponseType?, Error?) -> Void) where RequestType : Message, ResponseType : Message {
58 let now = Date()
59 let deadline = Date(timeInterval: 30, since: now)
60 let delay = TimeInterval(5)
61
62 self.invokeRetry(function: function,
63 request: request,
64 deadline: deadline,
65 delay: delay,
66 completion: completion)
67 }
68
69 private func invokeRetry<RequestType: Message, ResponseType: Message>(
70 function: String,
71 request: RequestType,
72 deadline: Date,
73 delay: TimeInterval,
74 completion: @escaping (ResponseType?, Error?) -> Void) {
75
76 self.underlyingInvocable.invoke(function: function,
77 request: request) { (response: ResponseType?, error: Error?) in
78 if self.retryableError(error: error) {
79 let now = Date()
80 let cutoff = Date(timeInterval: delay, since: now)
81 guard cutoff.compare(deadline) == ComparisonResult.orderedDescending else {
82 Thread.sleep(forTimeInterval: delay)
83 os_log("%{public}@ error: %{public}@ (retrying, now=%{public}@, deadline=%{public}@)", log: tplogDebug,
84 function,
85 "\(String(describing: error))",
86 "\(String(describing: now))",
87 "\(String(describing: deadline))")
88 self.invokeRetry(function: function,
89 request: request,
90 deadline: deadline,
91 delay: delay,
92 completion: completion)
93 return
94 }
95 }
96 completion(response, error)
97 }
98 }
99 }
100
101 public class MyCodeConnection: CloudKitCode.Invocable {
102 private let serviceName: String
103 private let container: CKContainer
104 private let databaseScope: CKDatabase.Scope
105 private let local: Bool
106 private let queue: DispatchQueue
107
108 internal init(service: String, container: CKContainer, databaseScope: CKDatabase.Scope, local: Bool) {
109 self.serviceName = service
110 self.container = container
111 self.databaseScope = databaseScope
112 self.local = local
113 self.queue = DispatchQueue(label: "MyCodeConnection", qos: .userInitiated)
114 }
115
116 /// Temporary stand-in until xcinverness moves to a swift-grpc plugin.
117 /// Intended to be used by protoc-generated code only
118 public func invoke<RequestType: Message, ResponseType: Message>(
119 function: String, request: RequestType,
120 completion: @escaping (ResponseType?, Error?) -> Void) {
121
122 // Hack to fool CloudKit, real solution is tracked in <rdar://problem/49086080>
123 self.queue.async {
124
125 let operation = CodeOperation<RequestType, ResponseType>(
126 service: self.serviceName,
127 functionName: function,
128 request: request,
129 destinationServer: self.local ? .local : .default)
130
131 // As each UUID finishes, log it.
132 let requestCompletion = { (requestInfo: CKRequestInfo?) -> Void in
133 if let requestUUID = requestInfo?.requestUUID {
134 os_log("ckoperation request finished: %{public}@ %{public}@", log: tplogDebug, function, requestUUID)
135 }
136 }
137 operation.requestCompletedBlock = requestCompletion
138
139 let loggingCompletion = { (response: ResponseType?, error: Error?) -> Void in
140 os_log("%@(%@): %@, error: %@",
141 log: tplogDebug,
142 function,
143 "\(String(describing: request))",
144 "\(String(describing: response))",
145 "\(String(describing: error))")
146 completion(response, error)
147 }
148 operation.codeOperationCompletionBlock = loggingCompletion
149
150 /* Same convenience API properties that we specify in CKContainer / CKDatabase */
151 operation.queuePriority = .low
152
153 // One alternative here would be to not set any QoS and trust the
154 // QoS propagation to do what's right. But there is also some benefit in
155 // just using a lower CPU QoS because it should be hard to measure for the
156 // casual adopter.
157 operation.qualityOfService = .userInitiated
158
159 operation.configuration.discretionaryNetworkBehavior = .nonDiscretionary
160 operation.configuration.automaticallyRetryNetworkFailures = false
161 operation.configuration.isCloudKitSupportOperation = true
162
163 operation.configuration.sourceApplicationBundleIdentifier = CuttlefishPushTopicBundleIdentifier
164
165 let database = self.container.database(with: self.databaseScope)
166
167 database.add(operation)
168 }
169 }
170 }
171
172 protocol ContainerNameToCuttlefishInvocable {
173 func client(container: String) -> CloudKitCode.Invocable
174 }
175
176 class CKCodeCuttlefishInvocableCreator: ContainerNameToCuttlefishInvocable {
177 func client(container: String) -> CloudKitCode.Invocable {
178 // Set up Cuttlefish client connection
179 let ckContainer = CKContainer(identifier: container)
180
181 // Cuttlefish is using its own push topic.
182 // To register for this push topic, we need to issue CK operations with a specific bundle identifier
183 ckContainer.sourceApplicationBundleIdentifier = CuttlefishPushTopicBundleIdentifier
184
185 let ckDatabase = ckContainer.privateCloudDatabase
186 return MyCodeConnection(service: "Cuttlefish", container: ckContainer,
187 databaseScope: ckDatabase.databaseScope, local: false)
188 }
189 }
190
191 // A collection of Containers.
192 // When a Container of a given name is requested, it is created if it did not already exist.
193 // Methods may be invoked concurrently.
194 class ContainerMap {
195 private let queue = DispatchQueue(label: "com.apple.security.TrustedPeersHelper.ContainerMap")
196
197 let invocableCreator: ContainerNameToCuttlefishInvocable
198
199 init(invocableCreator: ContainerNameToCuttlefishInvocable) {
200 self.invocableCreator = invocableCreator
201 }
202
203 // Only access containers while executing on queue
204 private var containers: [ContainerName: Container] = [:]
205
206 func findOrCreate(name: ContainerName) throws -> Container {
207 return try queue.sync {
208 if let container = self.containers[name] {
209 return container
210 } else {
211
212 // Set up Core Data stack
213 let persistentStoreURL = ContainerMap.urlForPersistentStore(name: name)
214 let description = NSPersistentStoreDescription(url: persistentStoreURL)
215
216 // Wrap whatever we're given in a magically-retrying layer
217 let cuttlefishInvocable = self.invocableCreator.client(container: name.container)
218 let retryingInvocable = RetryingInvocable(retry: cuttlefishInvocable)
219 let cuttlefish = CuttlefishAPIAsyncClient(invocable: retryingInvocable)
220
221 let container = try Container(name: name, persistentStoreDescription: description,
222 cuttlefish: cuttlefish)
223 self.containers[name] = container
224 return container
225 }
226 }
227 }
228
229 static func urlForPersistentStore(name: ContainerName) -> URL {
230 let filename = name.container + "-" + name.context + ".TrustedPeersHelper.db"
231 return SecCopyURLForFileInKeychainDirectory(filename as CFString) as URL
232 }
233 }