@capawesome/capacitor-live-update 6.4.1 → 6.5.0
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.
- package/README.md +2 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +351 -251
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +71 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +42 -59
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/Manifest.java +58 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/ManifestItem.java +37 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +19 -27
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetChannelResult.java +2 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/enums/ArtifactType.java +6 -0
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/Classes/Manifest.swift +20 -0
- package/ios/Plugin/Enums/ArtifactType.swift +4 -0
- package/ios/Plugin/{Classes → Enums}/CustomError.swift +3 -0
- package/ios/Plugin/LiveUpdate.swift +270 -199
- package/ios/Plugin/LiveUpdateHttpClient.swift +42 -0
- package/ios/Plugin/LiveUpdatePlugin.swift +65 -63
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +2 -3
- package/ios/Plugin/Protocols/Api/ManifestItem.swift +5 -0
- package/package.json +1 -1
|
@@ -4,22 +4,31 @@ import SSZipArchive
|
|
|
4
4
|
import Capacitor
|
|
5
5
|
import Alamofire
|
|
6
6
|
import CommonCrypto
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
// swiftlint:disable type_body_length
|
|
9
9
|
@objc public class LiveUpdate: NSObject {
|
|
10
|
+
private let bundlesDirectory = "NoCloud/ionic_built_snapshots" // DO NOT CHANGE! (See https://dub.sh/BLluidt)
|
|
10
11
|
private let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
11
12
|
private let config: LiveUpdateConfig
|
|
12
|
-
private let defaultWebAssetDir = "public" // DO NOT CHANGE (See https://dub.sh/Buvz4yj)
|
|
13
|
-
private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE (See https://dub.sh/ceDl0zT)
|
|
13
|
+
private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
|
|
14
|
+
private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
|
|
15
|
+
private let host: String
|
|
16
|
+
private let httpClient: LiveUpdateHttpClient
|
|
14
17
|
private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
|
|
18
|
+
private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
|
|
15
19
|
private let plugin: LiveUpdatePlugin
|
|
16
|
-
private let bundlesDirectory = "NoCloud/ionic_built_snapshots" // DO NOT CHANGE (See https://dub.sh/BLluidt)
|
|
17
20
|
private let preferences: LiveUpdatePreferences
|
|
18
21
|
|
|
19
22
|
private var rollbackDispatchWorkItem: DispatchWorkItem?
|
|
20
23
|
|
|
21
24
|
init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
|
|
22
25
|
self.config = config
|
|
26
|
+
if (config.location != nil) && config.location == "eu" {
|
|
27
|
+
self.host = "api.cloud.capawesome.eu"
|
|
28
|
+
} else {
|
|
29
|
+
self.host = "api.cloud.capawesome.io"
|
|
30
|
+
}
|
|
31
|
+
self.httpClient = LiveUpdateHttpClient(config: config)
|
|
23
32
|
self.plugin = plugin
|
|
24
33
|
self.preferences = LiveUpdatePreferences()
|
|
25
34
|
super.init()
|
|
@@ -51,26 +60,18 @@ import CommonCrypto
|
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
|
|
54
|
-
@objc public func downloadBundle(_ options: DownloadBundleOptions
|
|
63
|
+
@objc public func downloadBundle(_ options: DownloadBundleOptions) async throws {
|
|
55
64
|
let bundleId = options.getBundleId()
|
|
56
65
|
let checksum = options.getChecksum()
|
|
57
66
|
let url = options.getUrl()
|
|
58
67
|
|
|
59
68
|
// Check if the bundle already exists
|
|
60
69
|
if hasBundle(bundleId: bundleId) {
|
|
61
|
-
|
|
62
|
-
completion(error)
|
|
63
|
-
return
|
|
70
|
+
throw CustomError.bundleAlreadyExists
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
// Download the bundle
|
|
67
|
-
|
|
68
|
-
if let error = error {
|
|
69
|
-
completion(error)
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
completion(nil)
|
|
73
|
-
})
|
|
74
|
+
try await downloadBundleOfTypeZip(bundleId: bundleId, url: url, checksum: checksum)
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
@objc public func getBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
@@ -165,68 +166,60 @@ import CommonCrypto
|
|
|
165
166
|
completion(nil)
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
@objc public func sync(
|
|
169
|
+
@objc public func sync() async throws -> SyncResult {
|
|
169
170
|
// Fetch the latest bundle
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
guard let response = try await fetchLatestBundle() else {
|
|
172
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "No update available.")
|
|
173
|
+
return SyncResult(nextBundleId: nil)
|
|
174
|
+
}
|
|
175
|
+
let artifactType = response.artifactType
|
|
176
|
+
let latestBundleId = response.bundleId
|
|
177
|
+
let downloadUrl = response.url
|
|
178
|
+
// Check if bundle already exists
|
|
179
|
+
if hasBundle(bundleId: latestBundleId) {
|
|
180
|
+
var nextBundleId: String?
|
|
181
|
+
let currentBundleId = self.getCurrentBundleId()
|
|
182
|
+
if latestBundleId != currentBundleId {
|
|
183
|
+
// Set the next bundle
|
|
184
|
+
setNextBundle(bundleId: latestBundleId)
|
|
185
|
+
nextBundleId = latestBundleId
|
|
177
186
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
let result = SyncResult(nextBundleId: nextBundleId)
|
|
190
|
-
completion(result, nil)
|
|
191
|
-
} else {
|
|
192
|
-
// Download and unzip the bundle
|
|
193
|
-
self.downloadBundle(bundleId: latestBundleId, checksum: response.checksum, signature: response.signature, url: response.url, completion: { error in
|
|
194
|
-
if let error = error {
|
|
195
|
-
completion(nil, error)
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
// Set the next bundle
|
|
199
|
-
self.setNextBundle(bundleId: latestBundleId)
|
|
200
|
-
let result = SyncResult(nextBundleId: latestBundleId)
|
|
201
|
-
completion(result, nil)
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
})
|
|
187
|
+
return SyncResult(nextBundleId: nextBundleId)
|
|
188
|
+
}
|
|
189
|
+
// Download the bundle
|
|
190
|
+
if artifactType == .manifest {
|
|
191
|
+
try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
|
|
192
|
+
} else {
|
|
193
|
+
try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl, checksum: nil)
|
|
194
|
+
}
|
|
195
|
+
// Set the next bundle
|
|
196
|
+
setNextBundle(bundleId: latestBundleId)
|
|
197
|
+
return SyncResult(nextBundleId: latestBundleId)
|
|
206
198
|
}
|
|
207
199
|
|
|
208
|
-
private func addBundle(bundleId: String,
|
|
209
|
-
//
|
|
210
|
-
self.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
completion(CustomError.bundleIndexHtmlMissing)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
200
|
+
private func addBundle(bundleId: String, directory: URL) throws {
|
|
201
|
+
// Search folder with index.html file
|
|
202
|
+
guard let indexHtmlFile = self.searchIndexHtmlFile(url: directory) else {
|
|
203
|
+
throw CustomError.bundleIndexHtmlMissing
|
|
204
|
+
}
|
|
216
205
|
|
|
217
|
-
|
|
218
|
-
|
|
206
|
+
// Create the bundles directory if it does not exist
|
|
207
|
+
self.createBundlesDirectory()
|
|
219
208
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
209
|
+
// Move the unzipped files to the bundles directory
|
|
210
|
+
let bundlePath = self.buildBundlePathFor(bundleId: bundleId)
|
|
211
|
+
try FileManager.default.moveItem(atPath: indexHtmlFile.deletingLastPathComponent().path, toPath: bundlePath)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func addBundleOfTypeManifest(bundleId: String, directory: URL) async throws {
|
|
215
|
+
try addBundle(bundleId: bundleId, directory: directory)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private func addBundleOfTypeZip(bundleId: String, zipFile: URL) async throws {
|
|
219
|
+
// Unzip the bundle
|
|
220
|
+
let unzippedDirectory = self.unzipFile(zipFile: zipFile)
|
|
221
|
+
// Add the bundle
|
|
222
|
+
try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
|
|
230
223
|
}
|
|
231
224
|
|
|
232
225
|
private func buildCapacitorServerPathFor(bundleId: String) -> String {
|
|
@@ -240,22 +233,40 @@ import CommonCrypto
|
|
|
240
233
|
}
|
|
241
234
|
|
|
242
235
|
private func buildBundlePathFor(bundleId: String) -> String {
|
|
236
|
+
return buildBundleURLFor(bundleId: bundleId).path
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func buildBundleURLFor(bundleId: String) -> URL {
|
|
243
240
|
let url = libraryDirectoryUrl.appendingPathComponent(bundlesDirectory).appendingPathComponent(bundleId)
|
|
244
|
-
return url
|
|
241
|
+
return url
|
|
245
242
|
}
|
|
246
243
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
244
|
+
private func copyCurrentBundleFile(href: String, toDirectory: URL) throws {
|
|
245
|
+
guard let currentBundleId = getCurrentBundleId() else {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let destination = toDirectory.appendingPathComponent(href)
|
|
250
|
+
let parentDirectory = destination.deletingLastPathComponent()
|
|
251
|
+
try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
252
|
+
|
|
253
|
+
let sourceURL: URL
|
|
254
|
+
if currentBundleId == defaultWebAssetDir {
|
|
255
|
+
guard let file = Bundle.main.url(forResource: href, withExtension: nil, subdirectory: defaultWebAssetDir) else {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
sourceURL = file
|
|
259
|
+
} else {
|
|
260
|
+
sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try FileManager.default.copyItem(at: sourceURL, to: destination)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private func copyCurrentBundleFiles(hrefs: [String], toDirectory: URL) throws {
|
|
267
|
+
for href in hrefs {
|
|
268
|
+
try copyCurrentBundleFile(href: href, toDirectory: toDirectory)
|
|
269
|
+
}
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
private func createBundlesDirectory() {
|
|
@@ -270,14 +281,19 @@ import CommonCrypto
|
|
|
270
281
|
}
|
|
271
282
|
}
|
|
272
283
|
|
|
284
|
+
private func createTemporaryDirectory() throws -> URL {
|
|
285
|
+
let temporaryDirectory = cachesDirectoryUrl.appendingPathComponent(UUID().uuidString)
|
|
286
|
+
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
287
|
+
return temporaryDirectory
|
|
288
|
+
}
|
|
289
|
+
|
|
273
290
|
private func deleteBundle(bundleId: String) throws {
|
|
274
291
|
// Delete the bundle directory
|
|
275
292
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
276
293
|
try FileManager.default.removeItem(atPath: path)
|
|
277
294
|
// Reset the next bundle if it is the deleted bundle
|
|
278
|
-
let currentBundleId = getCurrentBundleId()
|
|
279
295
|
let nextBundleId = getNextBundleId()
|
|
280
|
-
if bundleId ==
|
|
296
|
+
if bundleId == nextBundleId {
|
|
281
297
|
setNextCapacitorServerPathToDefaultWebAssetDir()
|
|
282
298
|
}
|
|
283
299
|
}
|
|
@@ -298,94 +314,87 @@ import CommonCrypto
|
|
|
298
314
|
}
|
|
299
315
|
}
|
|
300
316
|
|
|
301
|
-
private func
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if
|
|
311
|
-
|
|
312
|
-
completion(CustomError.signatureMissing)
|
|
313
|
-
return
|
|
314
|
-
}
|
|
315
|
-
// Verify the signature
|
|
316
|
-
do {
|
|
317
|
-
let verified = try self.verifySignatureForFile(url: url, signature: signature, publicKey: publicKey)
|
|
318
|
-
if !verified {
|
|
319
|
-
completion(CustomError.signatureVerificationFailed)
|
|
320
|
-
return
|
|
321
|
-
}
|
|
322
|
-
} catch {
|
|
323
|
-
completion(CustomError.signatureVerificationFailed)
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
// Verify the checksum
|
|
328
|
-
else if let expectedChecksum = checksum {
|
|
329
|
-
// Calculate the checksum
|
|
330
|
-
do {
|
|
331
|
-
let receivedChecksum = try self.getChecksumForFile(url: url)
|
|
332
|
-
if receivedChecksum != expectedChecksum {
|
|
333
|
-
completion(CustomError.checksumMismatch)
|
|
334
|
-
return
|
|
335
|
-
}
|
|
336
|
-
} catch {
|
|
337
|
-
completion(CustomError.checksumCalculationFailed)
|
|
338
|
-
return
|
|
339
|
-
}
|
|
317
|
+
private func downloadAndVerifyFile(url: String, file: URL, checksum: String?) async throws {
|
|
318
|
+
let destination: DownloadRequest.Destination = { _, _ in
|
|
319
|
+
return (file, [.createIntermediateDirectories])
|
|
320
|
+
}
|
|
321
|
+
let urlComponents = URLComponents(string: url)!
|
|
322
|
+
let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination)
|
|
323
|
+
if let error = result.error {
|
|
324
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
|
|
325
|
+
if let urlError = error.underlyingError as? URLError {
|
|
326
|
+
if urlError.code == .timedOut {
|
|
327
|
+
throw urlError
|
|
340
328
|
}
|
|
341
|
-
|
|
342
|
-
// Add the bundle
|
|
343
|
-
self.addBundle(bundleId: bundleId, zipFile: url, completion: { error in
|
|
344
|
-
// Delete the temporary file
|
|
345
|
-
do {
|
|
346
|
-
try FileManager.default.removeItem(atPath: url.path)
|
|
347
|
-
} catch {
|
|
348
|
-
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete temporary file.")
|
|
349
|
-
}
|
|
350
|
-
if let error = error {
|
|
351
|
-
completion(error)
|
|
352
|
-
return
|
|
353
|
-
}
|
|
354
|
-
completion(nil)
|
|
355
|
-
})
|
|
356
329
|
}
|
|
357
|
-
|
|
330
|
+
throw CustomError.downloadFailed
|
|
331
|
+
}
|
|
332
|
+
guard let response = result.response else {
|
|
333
|
+
throw CustomError.unknown
|
|
334
|
+
}
|
|
335
|
+
let checksum = checksum == nil ? LiveUpdateHttpClient.getChecksumFromResponse(response: response) : checksum
|
|
336
|
+
let signature = LiveUpdateHttpClient.getSignatureFromResponse(response: response)
|
|
337
|
+
try verifyFile(url: file, checksum: checksum, signature: signature)
|
|
358
338
|
}
|
|
359
339
|
|
|
360
|
-
private func
|
|
361
|
-
let
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
default:
|
|
377
|
-
completion(nil, CustomError.downloadFailed)
|
|
378
|
-
}
|
|
379
|
-
} else {
|
|
380
|
-
completion(nil, CustomError.downloadFailed)
|
|
340
|
+
private func downloadBundleFile(baseUrl: String, href: String, directory: URL) async throws -> URL {
|
|
341
|
+
let fileURL = directory.appendingPathComponent(href)
|
|
342
|
+
var parameters = [String: String]()
|
|
343
|
+
parameters["href"] = href
|
|
344
|
+
var urlComponents = URLComponents(string: baseUrl)!
|
|
345
|
+
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
346
|
+
let url = urlComponents.string!
|
|
347
|
+
try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil)
|
|
348
|
+
return fileURL
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func downloadBundleFiles(url: String, hrefs: [String], directory: URL) async throws {
|
|
352
|
+
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
353
|
+
for href in hrefs {
|
|
354
|
+
group.addTask {
|
|
355
|
+
_ = try await self.downloadBundleFile(baseUrl: url, href: href, directory: directory)
|
|
381
356
|
}
|
|
382
|
-
return
|
|
383
357
|
}
|
|
384
|
-
|
|
358
|
+
|
|
359
|
+
try await group.waitForAll()
|
|
385
360
|
}
|
|
386
361
|
}
|
|
387
362
|
|
|
388
|
-
private func
|
|
363
|
+
private func downloadBundleOfTypeManifest(bundleId: String, url: String) async throws {
|
|
364
|
+
// Create a temporary directory
|
|
365
|
+
let temporaryDirectory = try createTemporaryDirectory()
|
|
366
|
+
// Download the latest manifest
|
|
367
|
+
let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory)
|
|
368
|
+
let latestManifest = try loadManifest(file: latestManifestFile)
|
|
369
|
+
// Load the current manifest
|
|
370
|
+
let currentManifest = try loadCurrentManifest()
|
|
371
|
+
// Compare the manifests
|
|
372
|
+
var itemsToCopy = [ManifestItem]()
|
|
373
|
+
var itemsToDownload = [ManifestItem]()
|
|
374
|
+
if let currentManifest = currentManifest {
|
|
375
|
+
itemsToCopy.append(contentsOf: Manifest.findDuplicateItems(latestManifest, currentManifest))
|
|
376
|
+
itemsToDownload.append(contentsOf: Manifest.findMissingItems(latestManifest, currentManifest))
|
|
377
|
+
} else {
|
|
378
|
+
itemsToDownload.append(contentsOf: latestManifest.items)
|
|
379
|
+
}
|
|
380
|
+
// Copy the files
|
|
381
|
+
try self.copyCurrentBundleFiles(hrefs: itemsToCopy.map { $0.href }, toDirectory: temporaryDirectory)
|
|
382
|
+
// Download the files
|
|
383
|
+
try await self.downloadBundleFiles(url: url, hrefs: itemsToDownload.map { $0.href }, directory: temporaryDirectory)
|
|
384
|
+
// Add the bundle
|
|
385
|
+
try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private func downloadBundleOfTypeZip(bundleId: String, url: String, checksum: String?) async throws {
|
|
389
|
+
let timestamp = String(Int(Date().timeIntervalSince1970))
|
|
390
|
+
let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
|
|
391
|
+
// Download the bundle
|
|
392
|
+
try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum)
|
|
393
|
+
// Add the bundle
|
|
394
|
+
try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private func fetchLatestBundle() async throws -> GetLatestBundleResponse? {
|
|
389
398
|
var parameters = [String: String]()
|
|
390
399
|
parameters["appVersionCode"] = getVersionCode()
|
|
391
400
|
parameters["appVersionName"] = getVersionName()
|
|
@@ -393,40 +402,29 @@ import CommonCrypto
|
|
|
393
402
|
parameters["channelName"] = getChannel()
|
|
394
403
|
parameters["customId"] = preferences.getCustomId()
|
|
395
404
|
parameters["deviceId"] = getDeviceId()
|
|
396
|
-
parameters["osVersion"] = UIDevice.current.systemVersion
|
|
405
|
+
parameters["osVersion"] = await UIDevice.current.systemVersion
|
|
397
406
|
parameters["platform"] = "1"
|
|
398
407
|
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
399
|
-
var host = "api.cloud.capawesome.io"
|
|
400
|
-
if let location = config.location {
|
|
401
|
-
if location == "eu" {
|
|
402
|
-
host = "api.cloud.capawesome.eu"
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
408
|
var urlComponents = URLComponents(string: "https://\(host)/v1/apps/\(config.appId ?? "")/bundles/latest")!
|
|
406
409
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
CAPLog.print("[", LiveUpdatePlugin.tag, "]
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if let urlError = error.underlyingError as? URLError {
|
|
418
|
-
switch urlError.code {
|
|
419
|
-
case .timedOut:
|
|
420
|
-
completion(nil, CustomError.httpTimeout)
|
|
421
|
-
default:
|
|
422
|
-
completion(nil, error)
|
|
423
|
-
}
|
|
424
|
-
} else {
|
|
425
|
-
completion(nil, error)
|
|
410
|
+
let url = try urlComponents.asURL()
|
|
411
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
|
|
412
|
+
let response = try await self.httpClient.request(url: url, type: GetLatestBundleResponse.self)
|
|
413
|
+
if let data = response.data {
|
|
414
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] Latest bundle response: ", String(decoding: data, as: UTF8.self))
|
|
415
|
+
}
|
|
416
|
+
if let error = response.error {
|
|
417
|
+
if let urlError = error.underlyingError as? URLError {
|
|
418
|
+
if urlError.code == .timedOut {
|
|
419
|
+
throw urlError
|
|
426
420
|
}
|
|
427
|
-
return
|
|
428
421
|
}
|
|
429
|
-
|
|
422
|
+
return nil
|
|
423
|
+
}
|
|
424
|
+
if let value = response.value {
|
|
425
|
+
return value
|
|
426
|
+
} else {
|
|
427
|
+
return nil
|
|
430
428
|
}
|
|
431
429
|
}
|
|
432
430
|
|
|
@@ -455,6 +453,20 @@ import CommonCrypto
|
|
|
455
453
|
return channel
|
|
456
454
|
}
|
|
457
455
|
|
|
456
|
+
/// - Returns: The sha256 checksum of the file at the given URL.
|
|
457
|
+
private func getChecksumForFile(url: URL) throws -> String {
|
|
458
|
+
let handle = try FileHandle(forReadingFrom: url)
|
|
459
|
+
var hasher = SHA256()
|
|
460
|
+
while autoreleasepool(invoking: {
|
|
461
|
+
let nextChunk = handle.readData(ofLength: 2048)
|
|
462
|
+
guard !nextChunk.isEmpty else { return false }
|
|
463
|
+
hasher.update(data: nextChunk)
|
|
464
|
+
return true
|
|
465
|
+
}) { }
|
|
466
|
+
let digest = hasher.finalize()
|
|
467
|
+
return digest.map { String(format: "%02hhx", $0) }.joined()
|
|
468
|
+
}
|
|
469
|
+
|
|
458
470
|
/// - Returns: The current bundle ID (`public` for the built-in bundle) or `nil` if no view controller was found.
|
|
459
471
|
private func getCurrentBundleId() -> String? {
|
|
460
472
|
guard let path = getCurrentCapacitorServerPath() else {
|
|
@@ -510,6 +522,36 @@ import CommonCrypto
|
|
|
510
522
|
return FileManager.default.fileExists(atPath: path)
|
|
511
523
|
}
|
|
512
524
|
|
|
525
|
+
private func loadCurrentManifest() throws -> Manifest? {
|
|
526
|
+
guard let currentBundleId = getCurrentBundleId() else {
|
|
527
|
+
return nil
|
|
528
|
+
}
|
|
529
|
+
if currentBundleId == defaultWebAssetDir {
|
|
530
|
+
let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
|
|
531
|
+
let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
|
|
532
|
+
if let manifestFileUrl = manifestFileUrl {
|
|
533
|
+
return try loadManifest(file: manifestFileUrl)
|
|
534
|
+
} else {
|
|
535
|
+
return nil
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
|
|
539
|
+
let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
|
|
540
|
+
if manifestFileExists {
|
|
541
|
+
return try loadManifest(file: manifestFileUrl)
|
|
542
|
+
} else {
|
|
543
|
+
return nil
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private func loadManifest(file: URL) throws -> Manifest {
|
|
549
|
+
let data = try Data(contentsOf: file)
|
|
550
|
+
let decoder = JSONDecoder()
|
|
551
|
+
let items = try decoder.decode([ManifestItem].self, from: data)
|
|
552
|
+
return Manifest(items: items)
|
|
553
|
+
}
|
|
554
|
+
|
|
513
555
|
private func rollback() {
|
|
514
556
|
if getCurrentBundleId() == defaultWebAssetDir {
|
|
515
557
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
|
|
@@ -598,13 +640,42 @@ import CommonCrypto
|
|
|
598
640
|
rollbackDispatchWorkItem?.cancel()
|
|
599
641
|
}
|
|
600
642
|
|
|
601
|
-
private func unzipFile(zipFile: URL
|
|
643
|
+
private func unzipFile(zipFile: URL) -> URL {
|
|
602
644
|
let destinationDirectory = zipFile.deletingPathExtension()
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
645
|
+
SSZipArchive.unzipFile(atPath: zipFile.path, toDestination: destinationDirectory.path)
|
|
646
|
+
return destinationDirectory
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private func verifyFile(url: URL, checksum: String?, signature: String?) throws {
|
|
650
|
+
// Verify the signature
|
|
651
|
+
if let publicKey = self.config.publicKey {
|
|
652
|
+
guard let signature = signature else {
|
|
653
|
+
throw CustomError.signatureMissing
|
|
654
|
+
}
|
|
655
|
+
// Verify the signature
|
|
656
|
+
let verified: Bool
|
|
657
|
+
do {
|
|
658
|
+
verified = try self.verifySignatureForFile(url: url, signature: signature, publicKey: publicKey)
|
|
659
|
+
} catch {
|
|
660
|
+
throw CustomError.signatureVerificationFailed
|
|
661
|
+
}
|
|
662
|
+
if !verified {
|
|
663
|
+
throw CustomError.signatureVerificationFailed
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Verify the checksum
|
|
667
|
+
else if let expectedChecksum = checksum {
|
|
668
|
+
// Calculate the checksum
|
|
669
|
+
let receivedChecksum: String
|
|
670
|
+
do {
|
|
671
|
+
receivedChecksum = try self.getChecksumForFile(url: url)
|
|
672
|
+
} catch {
|
|
673
|
+
throw CustomError.checksumCalculationFailed
|
|
674
|
+
}
|
|
675
|
+
if receivedChecksum != expectedChecksum {
|
|
676
|
+
throw CustomError.checksumMismatch
|
|
677
|
+
}
|
|
678
|
+
}
|
|
608
679
|
}
|
|
609
680
|
|
|
610
681
|
private func verifySignatureForFile(url: URL, signature: String, publicKey: String) throws -> Bool {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import Alamofire
|
|
4
|
+
|
|
5
|
+
public class LiveUpdateHttpClient: NSObject {
|
|
6
|
+
|
|
7
|
+
private let config: LiveUpdateConfig
|
|
8
|
+
|
|
9
|
+
public static func getChecksumFromResponse(response: HTTPURLResponse) -> String? {
|
|
10
|
+
return response.allHeaderFields["x-checksum"] as? String // Must be lowercase!
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public static func getSignatureFromResponse(response: HTTPURLResponse) -> String? {
|
|
14
|
+
return response.allHeaderFields["x-signature"] as? String // Must be lowercase!
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
init(config: LiveUpdateConfig) {
|
|
18
|
+
self.config = config
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public func download(url: URL, destination: @escaping DownloadRequest.Destination) async throws -> AFDownloadResponse<Data> {
|
|
22
|
+
var request = URLRequest(url: url)
|
|
23
|
+
request.httpMethod = HTTPMethod.get.rawValue
|
|
24
|
+
request.timeoutInterval = Double(config.httpTimeout) / 1000.0
|
|
25
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
26
|
+
AF.download(request, to: destination).responseData { response in
|
|
27
|
+
continuation.resume(returning: response)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public func request<T: Decodable>(url: URL, type: T.Type) async throws -> AFDataResponse<T> {
|
|
33
|
+
var request = URLRequest(url: url)
|
|
34
|
+
request.httpMethod = HTTPMethod.get.rawValue
|
|
35
|
+
request.timeoutInterval = Double(config.httpTimeout) / 1000.0
|
|
36
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
37
|
+
AF.request(request).validate().responseDecodable(of: type) { response in
|
|
38
|
+
continuation.resume(returning: response)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|