@tokamakdev/platform-ios-simulator-x64 0.1.1-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,515 @@
1
+ import Foundation
2
+ import Network
3
+ import Security
4
+ import WebKit
5
+
6
+ #if os(macOS)
7
+ import AppKit
8
+ #else
9
+ import UIKit
10
+ #endif
11
+
12
+ private let failurePage =
13
+ "<h1>App failed to start</h1><p>Check startup-error.log for details.</p>"
14
+
15
+ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDelegate {
16
+ private let runtime: RuntimeHandle
17
+ private let pluginBridge: TokamakPluginBridge
18
+
19
+ init(runtime: RuntimeHandle, pluginBridge: TokamakPluginBridge) {
20
+ self.runtime = runtime
21
+ self.pluginBridge = pluginBridge
22
+ }
23
+
24
+ func webView(
25
+ _ webView: WKWebView,
26
+ didStartProvisionalNavigation navigation: WKNavigation?
27
+ ) {
28
+ pluginBridge.close()
29
+ }
30
+
31
+ func webView(
32
+ _ webView: WKWebView,
33
+ decidePolicyFor navigationAction: WKNavigationAction,
34
+ decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
35
+ ) {
36
+ guard let url = navigationAction.request.url else {
37
+ decisionHandler(.cancel)
38
+ return
39
+ }
40
+
41
+ if navigationAction.targetFrame?.isMainFrame == false {
42
+ decisionHandler(.allow)
43
+ return
44
+ }
45
+
46
+ if url.isAppOrigin(runtime.appHost) {
47
+ if navigationAction.targetFrame == nil {
48
+ webView.load(navigationAction.request)
49
+ decisionHandler(.cancel)
50
+ } else {
51
+ decisionHandler(.allow)
52
+ }
53
+ return
54
+ }
55
+
56
+ openExternal(url)
57
+ decisionHandler(.cancel)
58
+ }
59
+
60
+ func webView(
61
+ _ webView: WKWebView,
62
+ decidePolicyFor navigationResponse: WKNavigationResponse,
63
+ decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
64
+ ) {
65
+ guard
66
+ navigationResponse.isForMainFrame,
67
+ let url = navigationResponse.response.url
68
+ else {
69
+ decisionHandler(.allow)
70
+ return
71
+ }
72
+
73
+ if url.isAppOrigin(runtime.appHost) {
74
+ decisionHandler(.allow)
75
+ } else {
76
+ openExternal(url)
77
+ decisionHandler(.cancel)
78
+ }
79
+ }
80
+
81
+ func webView(
82
+ _ webView: WKWebView,
83
+ createWebViewWith configuration: WKWebViewConfiguration,
84
+ for navigationAction: WKNavigationAction,
85
+ windowFeatures: WKWindowFeatures
86
+ ) -> WKWebView? {
87
+ guard let url = navigationAction.request.url else { return nil }
88
+ if url.isAppOrigin(runtime.appHost) {
89
+ webView.load(navigationAction.request)
90
+ } else {
91
+ openExternal(url)
92
+ }
93
+ return nil
94
+ }
95
+
96
+ /// The app's own usage descriptions cover its origin; other origins keep
97
+ /// WebKit's prompt.
98
+ func webView(
99
+ _ webView: WKWebView,
100
+ requestMediaCapturePermissionFor origin: WKSecurityOrigin,
101
+ initiatedByFrame frame: WKFrameInfo,
102
+ type: WKMediaCaptureType,
103
+ decisionHandler: @escaping (WKPermissionDecision) -> Void
104
+ ) {
105
+ decisionHandler(origin.isAppOrigin(runtime.appHost) ? .grant : .prompt)
106
+ }
107
+
108
+ func webView(
109
+ _ webView: WKWebView,
110
+ didReceive challenge: URLAuthenticationChallenge,
111
+ completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
112
+ ) {
113
+ let space = challenge.protectionSpace
114
+ switch space.authenticationMethod {
115
+ case NSURLAuthenticationMethodServerTrust:
116
+ answer(
117
+ runtime.serverAuthority(host: space.host),
118
+ trust: space.serverTrust,
119
+ completion: completionHandler
120
+ )
121
+ case NSURLAuthenticationMethodClientCertificate:
122
+ answer(
123
+ runtime.clientIdentity(
124
+ host: space.host,
125
+ previousFailures: challenge.previousFailureCount
126
+ ),
127
+ completion: completionHandler
128
+ )
129
+ default:
130
+ completionHandler(.performDefaultHandling, nil)
131
+ }
132
+ }
133
+
134
+ func webView(
135
+ _ webView: WKWebView,
136
+ didFailProvisionalNavigation navigation: WKNavigation?,
137
+ withError error: Error
138
+ ) {
139
+ print("tokamak WebView navigation failed: \(error)")
140
+ }
141
+
142
+ private func openExternal(_ url: URL) {
143
+ #if os(iOS)
144
+ UIApplication.shared.open(url)
145
+ #else
146
+ NSWorkspace.shared.open(url)
147
+ #endif
148
+ }
149
+
150
+ private func answer(
151
+ _ material: AuthenticationMaterial<Data>,
152
+ trust: SecTrust?,
153
+ completion: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
154
+ ) {
155
+ switch material {
156
+ case .defaultHandling:
157
+ completion(.performDefaultHandling, nil)
158
+ case .cancel:
159
+ completion(.cancelAuthenticationChallenge, nil)
160
+ case .use(let authority):
161
+ guard
162
+ let trust,
163
+ let certificate = SecCertificateCreateWithData(nil, authority as CFData),
164
+ SecTrustSetAnchorCertificates(trust, [certificate] as CFArray) == errSecSuccess,
165
+ SecTrustSetAnchorCertificatesOnly(trust, true) == errSecSuccess
166
+ else {
167
+ completion(.cancelAuthenticationChallenge, nil)
168
+ return
169
+ }
170
+ let queue = DispatchQueue.global(qos: .userInitiated)
171
+ queue.async {
172
+ SecTrustEvaluateAsyncWithError(trust, queue) { trust, trusted, error in
173
+ DispatchQueue.main.async {
174
+ guard trusted else {
175
+ print("tokamak refused the gateway's certificate: \(String(describing: error))")
176
+ completion(.cancelAuthenticationChallenge, nil)
177
+ return
178
+ }
179
+ completion(.useCredential, URLCredential(trust: trust))
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ private func answer(
187
+ _ material: AuthenticationMaterial<(Data, Data)>,
188
+ completion: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
189
+ ) {
190
+ switch material {
191
+ case .defaultHandling:
192
+ completion(.performDefaultHandling, nil)
193
+ case .cancel:
194
+ completion(.cancelAuthenticationChallenge, nil)
195
+ case .use(let value):
196
+ guard let identity = identity(certificate: value.0, privateKey: value.1) else {
197
+ completion(.cancelAuthenticationChallenge, nil)
198
+ return
199
+ }
200
+ let credential = URLCredential(identity: identity, certificates: nil, persistence: .none)
201
+ completion(.useCredential, credential)
202
+ }
203
+ }
204
+
205
+ private func identity(certificate: Data, privateKey: Data) -> SecIdentity? {
206
+ guard let certificate = SecCertificateCreateWithData(nil, certificate as CFData) else {
207
+ return nil
208
+ }
209
+ let attributes: [CFString: Any] = [
210
+ kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
211
+ kSecAttrKeyClass: kSecAttrKeyClassPrivate,
212
+ kSecAttrKeySizeInBits: 256,
213
+ ]
214
+ guard let key = SecKeyCreateWithData(privateKey as CFData, attributes as CFDictionary, nil)
215
+ else {
216
+ return nil
217
+ }
218
+ return SecIdentityCreate(nil, certificate, key)
219
+ }
220
+ }
221
+
222
+ private final class TokamakController {
223
+ private let app: TokamakApp
224
+ private var runtime: RuntimeHandle?
225
+ private var navigation: NavigationDelegate?
226
+ private var pluginBridge: TokamakPluginBridge?
227
+ private weak var activeWebView: WKWebView?
228
+ private var dataStore: WKWebsiteDataStore?
229
+ private var proxyPort: UInt16?
230
+
231
+ init(app: TokamakApp) {
232
+ self.app = app
233
+ }
234
+
235
+ deinit {
236
+ pluginBridge?.close()
237
+ }
238
+
239
+ func start(frame: CGRect, completion: @escaping (WKWebView) -> Void) {
240
+ app.whenStarted { result in
241
+ switch result {
242
+ case .success(let runtime):
243
+ self.runtime = runtime
244
+ completion(self.webView(frame: frame, runtime: runtime))
245
+ case .failure:
246
+ completion(self.failureWebView(frame: frame))
247
+ }
248
+ }
249
+ }
250
+
251
+ func restoreGateway() {
252
+ guard let runtime, let dataStore else { return }
253
+ do {
254
+ let port = try runtime.restoreGateway()
255
+ guard port != proxyPort else { return }
256
+ setProxy(port: port, host: runtime.appHost, dataStore: dataStore)
257
+ proxyPort = port
258
+ activeWebView?.reload()
259
+ } catch {
260
+ print("tokamak gateway could not recover: \(error)")
261
+ }
262
+ }
263
+
264
+ private func webView(frame: CGRect, runtime: RuntimeHandle) -> WKWebView {
265
+ let configuration = WKWebViewConfiguration()
266
+ let dataStore = WKWebsiteDataStore.default()
267
+ let port = runtime.port
268
+ setProxy(port: port, host: runtime.appHost, dataStore: dataStore)
269
+ self.dataStore = dataStore
270
+ self.proxyPort = port
271
+ configuration.websiteDataStore = dataStore
272
+ // Video plays within the page, and media starts without a tap.
273
+ #if os(iOS)
274
+ configuration.allowsInlineMediaPlayback = true
275
+ #endif
276
+ configuration.mediaTypesRequiringUserActionForPlayback = []
277
+
278
+ let pluginBridge = TokamakPluginBridge(host: runtime.appHost, plugins: app.plugins)
279
+ pluginBridge.install(in: configuration.userContentController)
280
+ let navigation = NavigationDelegate(runtime: runtime, pluginBridge: pluginBridge)
281
+ self.navigation = navigation
282
+ let webView = WKWebView(frame: frame, configuration: configuration)
283
+ #if os(iOS)
284
+ webView.scrollView.bounces = false
285
+ #endif
286
+ webView.allowsLinkPreview = false
287
+ activeWebView = webView
288
+ pluginBridge.webView = webView
289
+ self.pluginBridge = pluginBridge
290
+ webView.navigationDelegate = navigation
291
+ webView.uiDelegate = navigation
292
+ webView.load(URLRequest(url: URL(string: "https://\(runtime.appHost)/")!))
293
+ return webView
294
+ }
295
+
296
+ private func setProxy(port: UInt16, host: String, dataStore: WKWebsiteDataStore) {
297
+ var proxy = ProxyConfiguration(
298
+ httpCONNECTProxy: .hostPort(host: "127.0.0.1", port: NWEndpoint.Port(rawValue: port)!)
299
+ )
300
+ proxy.matchDomains = [host]
301
+ proxy.allowFailover = false
302
+ dataStore.proxyConfigurations = [proxy]
303
+ }
304
+
305
+ private func failureWebView(frame: CGRect) -> WKWebView {
306
+ let webView = WKWebView(frame: frame)
307
+ webView.loadHTMLString(failurePage, baseURL: nil)
308
+ return webView
309
+ }
310
+ }
311
+
312
+ #if os(macOS)
313
+ private final class TokamakMacApplicationDelegate: NSObject, NSApplicationDelegate {
314
+ private lazy var app = TokamakApp()
315
+ private lazy var controller = TokamakController(app: app)
316
+ private var window: NSWindow?
317
+ /// Whether the app is shown and its window is not miniaturised.
318
+ private var visible = false
319
+
320
+ func applicationDidFinishLaunching(_ notification: Notification) {
321
+ let window = NSWindow(
322
+ contentRect: NSRect(x: 0, y: 0, width: 1024, height: 768),
323
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
324
+ backing: .buffered,
325
+ defer: false
326
+ )
327
+ window.title =
328
+ Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "tokamak"
329
+ window.center()
330
+ window.makeKeyAndOrderFront(nil)
331
+ NSApplication.shared.activate(ignoringOtherApps: true)
332
+ self.window = window
333
+ visible = !NSApplication.shared.isHidden
334
+ app.start(foreground: visible)
335
+ observeVisibility()
336
+
337
+ guard let content = window.contentView else { return }
338
+ controller.start(frame: content.bounds) { webView in
339
+ webView.autoresizingMask = [.width, .height]
340
+ content.addSubview(webView)
341
+ }
342
+ }
343
+
344
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
345
+ true
346
+ }
347
+
348
+ func application(
349
+ _ application: NSApplication,
350
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
351
+ ) {
352
+ app.didRegisterForRemoteNotifications(deviceToken: deviceToken)
353
+ }
354
+
355
+ func application(
356
+ _ application: NSApplication,
357
+ didFailToRegisterForRemoteNotificationsWithError error: Error
358
+ ) {
359
+ app.didFailToRegisterForRemoteNotifications(error: error)
360
+ }
361
+
362
+ func application(
363
+ _ application: NSApplication,
364
+ didReceiveRemoteNotification userInfo: [String: Any]
365
+ ) {
366
+ app.didReceiveRemoteNotification(userInfo) { _ in }
367
+ }
368
+
369
+ /// Delivers `resume` and `suspend` as the app is hidden or shown, and as
370
+ /// its window is miniaturised or restored.
371
+ private func observeVisibility() {
372
+ let changes = [
373
+ NSApplication.didHideNotification,
374
+ NSApplication.didUnhideNotification,
375
+ NSWindow.didMiniaturizeNotification,
376
+ NSWindow.didDeminiaturizeNotification,
377
+ ]
378
+ for change in changes {
379
+ _ = NotificationCenter.default.addObserver(forName: change, object: nil, queue: .main) {
380
+ [weak self] _ in
381
+ self?.visibilityChanged()
382
+ }
383
+ }
384
+ }
385
+
386
+ private func visibilityChanged() {
387
+ let visible = !NSApplication.shared.isHidden && window?.isMiniaturized == false
388
+ guard visible != self.visible else { return }
389
+ self.visible = visible
390
+ app.moved(toForeground: visible)
391
+ }
392
+ }
393
+
394
+ @main
395
+ private enum TokamakApplication {
396
+ static func main() {
397
+ let application = NSApplication.shared
398
+ let delegate = TokamakMacApplicationDelegate()
399
+ application.setActivationPolicy(.regular)
400
+ application.delegate = delegate
401
+ application.run()
402
+ }
403
+ }
404
+ #else
405
+ @main
406
+ private enum TokamakApplication {
407
+ /// Only an app that declares the `remote-notification` background mode
408
+ /// gets a delegate that receives remote notifications, which UIKit
409
+ /// otherwise warns about.
410
+ static func main() {
411
+ let modes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
412
+ let delegate: TokamakIOSApplicationDelegate.Type =
413
+ modes?.contains("remote-notification") == true
414
+ ? TokamakIOSPushApplicationDelegate.self : TokamakIOSApplicationDelegate.self
415
+ UIApplicationMain(
416
+ CommandLine.argc,
417
+ CommandLine.unsafeArgv,
418
+ nil,
419
+ NSStringFromClass(delegate)
420
+ )
421
+ }
422
+ }
423
+
424
+ private class TokamakIOSApplicationDelegate: UIResponder, UIApplicationDelegate {
425
+ /// Exists from launch, whether or not a scene connects.
426
+ let app = TokamakApp()
427
+
428
+ /// Starts the runtime once launch finishes. A scene a user launches the
429
+ /// app into has entered the foreground by then.
430
+ func application(
431
+ _ application: UIApplication,
432
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
433
+ ) -> Bool {
434
+ DispatchQueue.main.async {
435
+ self.app.start(foreground: self.app.hasForegroundScene)
436
+ }
437
+ return true
438
+ }
439
+
440
+ func application(
441
+ _ application: UIApplication,
442
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
443
+ ) {
444
+ app.didRegisterForRemoteNotifications(deviceToken: deviceToken)
445
+ }
446
+
447
+ func application(
448
+ _ application: UIApplication,
449
+ didFailToRegisterForRemoteNotificationsWithError error: Error
450
+ ) {
451
+ app.didFailToRegisterForRemoteNotifications(error: error)
452
+ }
453
+ }
454
+
455
+ private final class TokamakIOSPushApplicationDelegate: TokamakIOSApplicationDelegate {
456
+ func application(
457
+ _ application: UIApplication,
458
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any],
459
+ fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
460
+ ) {
461
+ app.didReceiveRemoteNotification(userInfo) { result in
462
+ switch result {
463
+ case .newData: completionHandler(.newData)
464
+ case .noData: completionHandler(.noData)
465
+ case .failed: completionHandler(.failed)
466
+ }
467
+ }
468
+ }
469
+ }
470
+
471
+ /// The Info.plist scene configuration names this class.
472
+ @objc(TokamakSceneDelegate)
473
+ private final class TokamakSceneDelegate: UIResponder, UIWindowSceneDelegate {
474
+ var window: UIWindow?
475
+ private var controller: TokamakController?
476
+ private var app: TokamakApp {
477
+ (UIApplication.shared.delegate as! TokamakIOSApplicationDelegate).app
478
+ }
479
+
480
+ func scene(
481
+ _ scene: UIScene,
482
+ willConnectTo session: UISceneSession,
483
+ options connectionOptions: UIScene.ConnectionOptions
484
+ ) {
485
+ guard let scene = scene as? UIWindowScene else { return }
486
+ let viewController = UIViewController()
487
+ let window = UIWindow(windowScene: scene)
488
+ window.rootViewController = viewController
489
+ window.makeKeyAndVisible()
490
+ self.window = window
491
+
492
+ let controller = TokamakController(app: app)
493
+ controller.start(frame: viewController.view.bounds) { webView in
494
+ webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
495
+ viewController.view.addSubview(webView)
496
+ }
497
+ self.controller = controller
498
+ }
499
+
500
+ func sceneWillEnterForeground(_ scene: UIScene) {
501
+ app.sceneWillEnterForeground()
502
+ controller?.restoreGateway()
503
+ }
504
+
505
+ func sceneDidEnterBackground(_ scene: UIScene) {
506
+ app.sceneDidEnterBackground()
507
+ }
508
+
509
+ /// Releases the scene's window and page, which a reconnection recreates.
510
+ func sceneDidDisconnect(_ scene: UIScene) {
511
+ window = nil
512
+ controller = nil
513
+ }
514
+ }
515
+ #endif
@@ -0,0 +1,36 @@
1
+ {
2
+ "tokamakVersion": "0.1.1",
3
+ "target": "ios-simulator-x64",
4
+ "variables": {
5
+ "build-number": {
6
+ "kind": "string",
7
+ "description": "CFBundleVersion, one to three period-separated integers; defaults to the app version"
8
+ },
9
+ "entitlements": {
10
+ "kind": "path",
11
+ "description": "Entitlements plist layered over the provisioning profile's entitlements at signing"
12
+ },
13
+ "plist": {
14
+ "kind": "path",
15
+ "description": "Info.plist whose values take precedence over generated and plugin values"
16
+ },
17
+ "provisioning-profile": {
18
+ "kind": "path",
19
+ "description": "Provisioning profile for manual signing, used with signing-identity"
20
+ },
21
+ "signing-identity": {
22
+ "kind": "string",
23
+ "description": "SHA-1 of the signing identity for manual signing, used with provisioning-profile"
24
+ },
25
+ "team-id": {
26
+ "kind": "string",
27
+ "description": "Apple Development team for automatic signing"
28
+ }
29
+ },
30
+ "pluginKeys": {
31
+ "class": "string",
32
+ "plist": "path",
33
+ "resources": "paths",
34
+ "sources": "paths"
35
+ }
36
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@tokamakdev/platform-ios-simulator-x64",
3
+ "version": "0.1.1-beta.100",
4
+ "description": "The tokamak platform pack for ios-simulator-x64",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/mantty/tokamak.git",
8
+ "directory": "tokamak-cli/npm/platform-packs/ios-simulator-x64"
9
+ },
10
+ "os": [
11
+ "darwin"
12
+ ],
13
+ "cpu": [
14
+ "x64"
15
+ ],
16
+ "preferUnplugged": true,
17
+ "files": [
18
+ "ios-simulator-x64"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ }
23
+ }