@capgo/cordova-updater 8.1.4 → 8.2.1
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/dist/esm/definitions.d.ts +116 -86
- package/dist/esm/definitions.js +2 -2
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/exec.js +11 -1
- package/dist/esm/exec.js.map +1 -1
- package/dist/esm/history.js +7 -7
- package/dist/esm/history.js.map +1 -1
- package/dist/plugin.cjs.js +13 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +13 -3
- package/dist/plugin.js.map +1 -1
- package/package.json +5 -3
- package/plugin.xml +9 -9
- package/src/android/CordovaUpdaterPlugin.java +173 -5
- package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +313 -55
- package/src/android/app/capgo/cordova/updater/CryptoCipher.java +51 -18
- package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +0 -1
- package/src/android/app/capgo/cordova/updater/DownloadService.java +402 -89
- package/src/android/app/capgo/cordova/updater/Version.java +312 -0
- package/src/android/capgo-cordova-updater.gradle +3 -5
- package/src/android/test/app/capgo/cordova/updater/BackgroundDownloadSettlementTest.java +29 -0
- package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +2 -12
- package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +0 -1
- package/src/android/test/app/capgo/cordova/updater/RsaContractTest.java +137 -0
- package/src/android/test/app/capgo/cordova/updater/SecurityHardeningTest.java +197 -0
- package/src/android/test/app/capgo/cordova/updater/SessionKeyRequiredTest.java +344 -0
- package/src/ios/CapgoSemanticVersion.swift +181 -0
- package/src/ios/CapgoUpdater.swift +427 -114
- package/src/ios/CordovaUpdaterPlugin.swift +171 -40
- package/src/ios/CryptoCipher.swift +41 -38
- package/src/ios/DelayUpdateUtils.swift +19 -45
- package/src/ios/Logger.swift +81 -5
- package/src/ios/RedirectPolicyDelegate.swift +53 -0
- package/src/ios/WebViewStatsReporter.swift +5 -0
- package/src/ios/ZipArchiveReader.swift +263 -0
- package/src/ios/ZipCentralDirectory.swift +283 -0
|
@@ -10,7 +10,6 @@ import Cordova
|
|
|
10
10
|
#endif
|
|
11
11
|
import UIKit
|
|
12
12
|
import WebKit
|
|
13
|
-
import Version
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Please read the Capacitor iOS Plugin Development Guide
|
|
@@ -88,7 +87,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
88
87
|
}()
|
|
89
88
|
|
|
90
89
|
public var implementation = CapgoUpdater()
|
|
91
|
-
private let pluginVersion: String = "8.1
|
|
90
|
+
private let pluginVersion: String = "8.2.1"
|
|
92
91
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
93
92
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
94
93
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -139,7 +138,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
139
138
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
140
139
|
private var updateUrl = ""
|
|
141
140
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
142
|
-
private var currentVersionNative:
|
|
141
|
+
private var currentVersionNative = CapgoSemanticVersion(major: 0, minor: 0, patch: 0)
|
|
143
142
|
private var currentBuildVersion: String = "0"
|
|
144
143
|
private var autoUpdate = false
|
|
145
144
|
private var autoUpdateMode = CordovaUpdaterPlugin.autoUpdateModeOff
|
|
@@ -200,6 +199,8 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
200
199
|
private var isLeavingPreviewForIncomingLink = false
|
|
201
200
|
private var previewTransitionClearWorkItem: DispatchWorkItem?
|
|
202
201
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
202
|
+
private var readyGuardArmed = false
|
|
203
|
+
private var readyGeneration = 0
|
|
203
204
|
|
|
204
205
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
205
206
|
|
|
@@ -243,7 +244,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
243
244
|
fatalError("Cannot get version name")
|
|
244
245
|
}
|
|
245
246
|
do {
|
|
246
|
-
currentVersionNative = try
|
|
247
|
+
currentVersionNative = try CapgoSemanticVersion(versionName)
|
|
247
248
|
} catch {
|
|
248
249
|
logger.error("Cannot parse versionName \(versionName)")
|
|
249
250
|
}
|
|
@@ -285,6 +286,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
285
286
|
periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(readConfigInt("periodCheckDelay", 0))
|
|
286
287
|
|
|
287
288
|
implementation.setPublicKey(readConfigString("publicKey") ?? "")
|
|
289
|
+
implementation.allowHttpsToHttpRedirect = readConfigBool("allowHttpsToHttpRedirect", false)
|
|
288
290
|
implementation.notifyDownloadRaw = notifyDownload
|
|
289
291
|
implementation.notifyListeners = { [weak self] eventName, data in
|
|
290
292
|
let emit = {
|
|
@@ -473,13 +475,21 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
473
475
|
)
|
|
474
476
|
}
|
|
475
477
|
|
|
476
|
-
private func
|
|
477
|
-
let script: String
|
|
478
|
+
private func keepUrlPathFlagScriptSource(enabled: Bool) -> String {
|
|
478
479
|
if enabled {
|
|
479
|
-
|
|
480
|
-
} else {
|
|
481
|
-
script = "(function(){ try { localStorage.removeItem('\(keepUrlPathFlagKey)'); } catch (err) {} delete window.__capgoKeepUrlPathAfterReload; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: false } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: false }); } window.dispatchEvent(evt); })();"
|
|
480
|
+
return "(function(){ try { localStorage.setItem('\(keepUrlPathFlagKey)', '1'); } catch (err) {} window.__capgoKeepUrlPathAfterReload = true; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: true } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: true }); } window.dispatchEvent(evt); })();"
|
|
482
481
|
}
|
|
482
|
+
return "(function(){ try { localStorage.removeItem('\(keepUrlPathFlagKey)'); } catch (err) {} delete window.__capgoKeepUrlPathAfterReload; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: false } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: false }); } window.dispatchEvent(evt); })();"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private func addKeepUrlPathDocumentStartScript(to controller: WKUserContentController, enabled: Bool) {
|
|
486
|
+
let userScript = WKUserScript(source: keepUrlPathFlagScriptSource(enabled: enabled), injectionTime: .atDocumentStart, forMainFrameOnly: true)
|
|
487
|
+
controller.addUserScript(userScript)
|
|
488
|
+
self.keepUrlPathFlagLastValue = enabled
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private func syncKeepUrlPathFlag(enabled: Bool) {
|
|
492
|
+
let script = keepUrlPathFlagScriptSource(enabled: enabled)
|
|
483
493
|
DispatchQueue.main.async { [weak self] in
|
|
484
494
|
guard let self = self, let webView = self.updaterWebView else {
|
|
485
495
|
return
|
|
@@ -545,8 +555,10 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
545
555
|
var dest: URL
|
|
546
556
|
if BundleInfo.ID_BUILTIN == id {
|
|
547
557
|
dest = Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
558
|
+
} else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
|
|
559
|
+
dest = bundleDir
|
|
548
560
|
} else {
|
|
549
|
-
dest =
|
|
561
|
+
dest = Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
550
562
|
}
|
|
551
563
|
|
|
552
564
|
if !FileManager.default.fileExists(atPath: dest.path) {
|
|
@@ -1594,7 +1606,19 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
1594
1606
|
throw makePreviewError("Invalid download URL")
|
|
1595
1607
|
}
|
|
1596
1608
|
|
|
1609
|
+
if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
1610
|
+
self.logger.error("Public key present but no valid session key provided")
|
|
1611
|
+
self.implementation.sendStats(action: "session_key_required", versionName: version)
|
|
1612
|
+
throw makePreviewError("Session key required when public key is present")
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1597
1615
|
var checksum = rawChecksum
|
|
1616
|
+
if manifestEntries == nil && checksum.isEmpty {
|
|
1617
|
+
self.logger.error("No checksum provided")
|
|
1618
|
+
self.implementation.sendStats(action: "checksum_required", versionName: version)
|
|
1619
|
+
throw makePreviewError("Checksum required")
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1598
1622
|
let next: BundleInfo
|
|
1599
1623
|
if let manifestEntries = manifestEntries {
|
|
1600
1624
|
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
@@ -1602,29 +1626,20 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
1602
1626
|
next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
|
|
1603
1627
|
}
|
|
1604
1628
|
|
|
1605
|
-
if
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
self.
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
1619
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
1620
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
1621
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
1622
|
-
let id = next.getId()
|
|
1623
|
-
let resDel = self.implementation.delete(id: id)
|
|
1624
|
-
if !resDel {
|
|
1625
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1629
|
+
if manifestEntries == nil {
|
|
1630
|
+
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
1631
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
1632
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
1633
|
+
if next.getChecksum() != checksum {
|
|
1634
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
1635
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
1636
|
+
let id = next.getId()
|
|
1637
|
+
let resDel = self.implementation.delete(id: id)
|
|
1638
|
+
if !resDel {
|
|
1639
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1640
|
+
}
|
|
1641
|
+
throw ObjectSavableError.checksum
|
|
1626
1642
|
}
|
|
1627
|
-
throw ObjectSavableError.checksum
|
|
1628
1643
|
}
|
|
1629
1644
|
|
|
1630
1645
|
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
@@ -1676,8 +1691,10 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
1676
1691
|
let id = self.implementation.getCurrentBundleId()
|
|
1677
1692
|
if BundleInfo.ID_BUILTIN == id {
|
|
1678
1693
|
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
1694
|
+
} else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
|
|
1695
|
+
return bundleDir
|
|
1679
1696
|
} else {
|
|
1680
|
-
return
|
|
1697
|
+
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
1681
1698
|
}
|
|
1682
1699
|
}
|
|
1683
1700
|
|
|
@@ -1685,6 +1702,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
1685
1702
|
let id = self.implementation.getCurrentBundleId()
|
|
1686
1703
|
let dest = self.currentReloadDestination()
|
|
1687
1704
|
logger.info("Reloading \(id)")
|
|
1705
|
+
self.armReadyGuard(webView: self.updaterWebView)
|
|
1688
1706
|
|
|
1689
1707
|
if self.keepUrlPathAfterReload {
|
|
1690
1708
|
self.syncKeepUrlPathFlag(enabled: true)
|
|
@@ -2889,11 +2907,19 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
2889
2907
|
self.saveCallForAsyncHandling(call)
|
|
2890
2908
|
DispatchQueue.global(qos: .utility).async {
|
|
2891
2909
|
let configDefaultChannel = self.readConfigString("defaultChannel", "")!
|
|
2892
|
-
let res = self.implementation.unsetChannel(
|
|
2910
|
+
let res = self.implementation.unsetChannel(
|
|
2911
|
+
defaultChannelKey: self.defaultChannelDefaultsKey,
|
|
2912
|
+
configDefaultChannel: configDefaultChannel,
|
|
2913
|
+
allowSetDefaultChannel: self.allowSetDefaultChannel
|
|
2914
|
+
)
|
|
2893
2915
|
if res.error != "" {
|
|
2894
|
-
|
|
2895
|
-
"
|
|
2896
|
-
|
|
2916
|
+
let errorCode = res.error == "disabled_by_config"
|
|
2917
|
+
? "disabled_by_config"
|
|
2918
|
+
: (res.error.contains("Channel URL") ? "missing_config" : "request_failed")
|
|
2919
|
+
let message = res.message.isEmpty ? res.error : res.message
|
|
2920
|
+
self.rejectCall(call, message: message, code: "UNSETCHANNEL_FAILED", data: [
|
|
2921
|
+
"message": message,
|
|
2922
|
+
"error": errorCode
|
|
2897
2923
|
])
|
|
2898
2924
|
} else {
|
|
2899
2925
|
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
@@ -3116,7 +3142,90 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
3116
3142
|
])
|
|
3117
3143
|
}
|
|
3118
3144
|
|
|
3145
|
+
static func shouldAcceptReadyCall(guardArmed: Bool, expectedGeneration: Int, reportedGeneration: Int?) -> Bool {
|
|
3146
|
+
if !guardArmed {
|
|
3147
|
+
return true
|
|
3148
|
+
}
|
|
3149
|
+
guard let reportedGeneration else {
|
|
3150
|
+
return false
|
|
3151
|
+
}
|
|
3152
|
+
return reportedGeneration == expectedGeneration
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
static func readyGenerationScript(_ generation: Int) -> String {
|
|
3156
|
+
return "(function(){window.__CAPGO_READY_GEN=\(generation);})();"
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
static func readyGenerationBootstrapScript(_ generation: Int) -> String {
|
|
3160
|
+
return """
|
|
3161
|
+
(function(){
|
|
3162
|
+
var slot = { value: \(generation) };
|
|
3163
|
+
window.__capgoReadyGenSlot = slot;
|
|
3164
|
+
Object.defineProperty(window, '__CAPGO_READY_GEN', { get: function() { return slot.value; }, configurable: true });
|
|
3165
|
+
})();
|
|
3166
|
+
"""
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
private func reinstallDocumentStartUserScripts(webView: WKWebView, readyGeneration: Int) {
|
|
3170
|
+
let controller = webView.configuration.userContentController
|
|
3171
|
+
controller.removeAllUserScripts()
|
|
3172
|
+
self.keepUrlPathFlagLastValue = nil
|
|
3173
|
+
let readyScript = WKUserScript(
|
|
3174
|
+
source: CordovaUpdaterPlugin.readyGenerationBootstrapScript(readyGeneration),
|
|
3175
|
+
injectionTime: .atDocumentStart,
|
|
3176
|
+
forMainFrameOnly: true
|
|
3177
|
+
)
|
|
3178
|
+
controller.addUserScript(readyScript)
|
|
3179
|
+
if self.keepUrlPathAfterReload {
|
|
3180
|
+
self.addKeepUrlPathDocumentStartScript(to: controller, enabled: true)
|
|
3181
|
+
}
|
|
3182
|
+
self.webViewStatsReporter?.reinstallDocumentStartScript(on: webView)
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
private func armReadyGuard(webView: WKWebView?) {
|
|
3186
|
+
self.readyGeneration += 1
|
|
3187
|
+
let generation = self.readyGeneration
|
|
3188
|
+
self.readyGuardArmed = true
|
|
3189
|
+
guard let webView else {
|
|
3190
|
+
logger.warn("Cannot stamp notifyAppReady generation without a webview")
|
|
3191
|
+
self.readyGuardArmed = false
|
|
3192
|
+
return
|
|
3193
|
+
}
|
|
3194
|
+
let installScripts = {
|
|
3195
|
+
self.reinstallDocumentStartUserScripts(webView: webView, readyGeneration: generation)
|
|
3196
|
+
}
|
|
3197
|
+
if Thread.isMainThread {
|
|
3198
|
+
installScripts()
|
|
3199
|
+
} else {
|
|
3200
|
+
DispatchQueue.main.sync(execute: installScripts)
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
private func reportedReadyGeneration(_ call: CAPPluginCall) -> Int? {
|
|
3205
|
+
if let value = call.getValue("loadGeneration") as? Int {
|
|
3206
|
+
return value
|
|
3207
|
+
}
|
|
3208
|
+
if let number = call.getValue("loadGeneration") as? NSNumber {
|
|
3209
|
+
return number.intValue
|
|
3210
|
+
}
|
|
3211
|
+
return nil
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
private func acceptsReadyCall(_ call: CAPPluginCall) -> Bool {
|
|
3215
|
+
let reported = self.readyGuardArmed ? self.reportedReadyGeneration(call) : nil
|
|
3216
|
+
return CordovaUpdaterPlugin.shouldAcceptReadyCall(
|
|
3217
|
+
guardArmed: self.readyGuardArmed,
|
|
3218
|
+
expectedGeneration: self.readyGeneration,
|
|
3219
|
+
reportedGeneration: reported
|
|
3220
|
+
)
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3119
3223
|
func notifyAppReady(_ call: CAPPluginCall) {
|
|
3224
|
+
if !self.acceptsReadyCall(call) {
|
|
3225
|
+
logger.info("Ignoring notifyAppReady from a page that is no longer current")
|
|
3226
|
+
call.resolve(["bundle": self.implementation.getCurrentBundle().toJSON()])
|
|
3227
|
+
return
|
|
3228
|
+
}
|
|
3120
3229
|
self.semaphoreDown()
|
|
3121
3230
|
let bundle = self.implementation.getCurrentBundle()
|
|
3122
3231
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
@@ -4123,6 +4232,28 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
4123
4232
|
if latestVersionName != "" && current.getVersionName() != latestVersionName {
|
|
4124
4233
|
do {
|
|
4125
4234
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
4235
|
+
if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
|
|
4236
|
+
self.logger.error("Public key present but no valid session key provided")
|
|
4237
|
+
self.implementation.sendStats(action: "session_key_required", versionName: latestVersionName)
|
|
4238
|
+
self.endBackGroundTaskWithNotif(
|
|
4239
|
+
msg: "Session key required when public key is present",
|
|
4240
|
+
latestVersionName: latestVersionName,
|
|
4241
|
+
current: current,
|
|
4242
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4243
|
+
)
|
|
4244
|
+
return
|
|
4245
|
+
}
|
|
4246
|
+
if res.manifest == nil && (res.checksum ?? "").isEmpty {
|
|
4247
|
+
self.logger.error("No checksum provided")
|
|
4248
|
+
self.implementation.sendStats(action: "checksum_required", versionName: latestVersionName)
|
|
4249
|
+
self.endBackGroundTaskWithNotif(
|
|
4250
|
+
msg: "Checksum required",
|
|
4251
|
+
latestVersionName: latestVersionName,
|
|
4252
|
+
current: current,
|
|
4253
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4254
|
+
)
|
|
4255
|
+
return
|
|
4256
|
+
}
|
|
4126
4257
|
var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
|
|
4127
4258
|
if nextImpl == nil || nextImpl?.isDeleted() == true {
|
|
4128
4259
|
if nextImpl?.isDeleted() == true {
|
|
@@ -4169,7 +4300,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
4169
4300
|
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
4170
4301
|
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
4171
4302
|
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
|
|
4172
|
-
if res.
|
|
4303
|
+
if res.manifest == nil && next.getChecksum() != res.checksum {
|
|
4173
4304
|
self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
|
|
4174
4305
|
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
4175
4306
|
let id = next.getId()
|
|
@@ -4584,8 +4715,8 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
|
|
|
4584
4715
|
// Determine update availability by comparing versions
|
|
4585
4716
|
if let availableVersion = availableVersion {
|
|
4586
4717
|
do {
|
|
4587
|
-
let currentVer = try
|
|
4588
|
-
let availableVer = try
|
|
4718
|
+
let currentVer = try CapgoSemanticVersion(currentVersionName)
|
|
4719
|
+
let availableVer = try CapgoSemanticVersion(availableVersion)
|
|
4589
4720
|
if availableVer > currentVer {
|
|
4590
4721
|
result["updateAvailability"] = AppUpdateAvailability.updateAvailable.rawValue
|
|
4591
4722
|
} else {
|
|
@@ -160,50 +160,46 @@ public struct CryptoCipher {
|
|
|
160
160
|
public static func calcChecksum(filePath: URL) -> String {
|
|
161
161
|
let bufferSize = checksumBufferBytes()
|
|
162
162
|
var sha256 = SHA256()
|
|
163
|
+
var readFailed = false
|
|
163
164
|
|
|
165
|
+
let fileHandle: FileHandle
|
|
164
166
|
do {
|
|
165
|
-
|
|
167
|
+
fileHandle = try FileHandle(forReadingFrom: filePath)
|
|
168
|
+
} catch {
|
|
169
|
+
logger.error("Cannot open file for checksum calculation")
|
|
170
|
+
logger.debug("Path: \(filePath.path), Error: \(error)")
|
|
171
|
+
return ""
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
defer {
|
|
166
175
|
do {
|
|
167
|
-
|
|
176
|
+
try fileHandle.close()
|
|
168
177
|
} catch {
|
|
169
|
-
logger.error("
|
|
170
|
-
logger.debug("
|
|
171
|
-
return ""
|
|
178
|
+
logger.error("Error closing file during checksum")
|
|
179
|
+
logger.debug("Error: \(error)")
|
|
172
180
|
}
|
|
181
|
+
}
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
while autoreleasepool(invoking: {
|
|
184
|
+
let fileData: Data
|
|
185
|
+
do {
|
|
186
|
+
fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
|
|
187
|
+
} catch {
|
|
188
|
+
logger.error("Error reading file during checksum")
|
|
189
|
+
logger.debug("Error: \(error)")
|
|
190
|
+
readFailed = true
|
|
191
|
+
return false
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if fileData.count > 0 {
|
|
194
|
-
sha256.update(data: fileData)
|
|
195
|
-
return true // Continue
|
|
196
|
-
} else {
|
|
197
|
-
return false // End of file
|
|
198
|
-
}
|
|
199
|
-
}) {}
|
|
194
|
+
if fileData.count > 0 {
|
|
195
|
+
sha256.update(data: fileData)
|
|
196
|
+
return true // Continue
|
|
197
|
+
} else {
|
|
198
|
+
return false // End of file
|
|
199
|
+
}
|
|
200
|
+
}) {}
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
} catch {
|
|
203
|
-
logger.error("Cannot calculate checksum")
|
|
204
|
-
logger.debug("Path: \(filePath.path), Error: \(error)")
|
|
205
|
-
return ""
|
|
206
|
-
}
|
|
202
|
+
return readFailed ? "" : hexString(from: sha256)
|
|
207
203
|
}
|
|
208
204
|
|
|
209
205
|
final class RunningChecksum {
|
|
@@ -222,8 +218,7 @@ public struct CryptoCipher {
|
|
|
222
218
|
}
|
|
223
219
|
|
|
224
220
|
static func hexString(from sha256: SHA256) -> String {
|
|
225
|
-
|
|
226
|
-
return copy.finalize().compactMap { String(format: "%02x", $0) }.joined()
|
|
221
|
+
return sha256.finalize().compactMap { String(format: "%02x", $0) }.joined()
|
|
227
222
|
}
|
|
228
223
|
|
|
229
224
|
static func shortPathKey(_ fileName: String) -> String {
|
|
@@ -232,8 +227,16 @@ public struct CryptoCipher {
|
|
|
232
227
|
return String(hexString(from: sha256).prefix(16))
|
|
233
228
|
}
|
|
234
229
|
|
|
230
|
+
public static func isValidSessionKey(_ sessionKey: String) -> Bool {
|
|
231
|
+
if sessionKey.isEmpty {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
let sessionKeyParts = sessionKey.components(separatedBy: ":")
|
|
235
|
+
return sessionKeyParts.count == 2 && !sessionKeyParts[0].isEmpty && !sessionKeyParts[1].isEmpty
|
|
236
|
+
}
|
|
237
|
+
|
|
235
238
|
public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
|
|
236
|
-
if publicKey.isEmpty || sessionKey
|
|
239
|
+
if publicKey.isEmpty || !isValidSessionKey(sessionKey) {
|
|
237
240
|
logger.info("Encryption not set, no public key or session, ignored")
|
|
238
241
|
return
|
|
239
242
|
}
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
//
|
|
14
14
|
|
|
15
15
|
import Foundation
|
|
16
|
-
import Version
|
|
17
16
|
|
|
18
17
|
public class DelayUpdateUtils {
|
|
19
18
|
|
|
@@ -23,7 +22,7 @@ public class DelayUpdateUtils {
|
|
|
23
22
|
// swiftlint:enable identifier_name
|
|
24
23
|
private let logger: Logger
|
|
25
24
|
|
|
26
|
-
private let currentVersionNative:
|
|
25
|
+
private let currentVersionNative: CapgoSemanticVersion
|
|
27
26
|
|
|
28
27
|
public enum CancelDelaySource {
|
|
29
28
|
case killed
|
|
@@ -39,7 +38,7 @@ public class DelayUpdateUtils {
|
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
public init(currentVersionNative:
|
|
41
|
+
public init(currentVersionNative: CapgoSemanticVersion, logger: Logger) {
|
|
43
42
|
self.currentVersionNative = currentVersionNative
|
|
44
43
|
self.logger = logger
|
|
45
44
|
}
|
|
@@ -117,7 +116,7 @@ public class DelayUpdateUtils {
|
|
|
117
116
|
case "nativeVersion":
|
|
118
117
|
if let value = value, !value.isEmpty {
|
|
119
118
|
do {
|
|
120
|
-
let versionLimit = try
|
|
119
|
+
let versionLimit = try CapgoSemanticVersion(value)
|
|
121
120
|
if currentVersionNative >= versionLimit {
|
|
122
121
|
// swiftlint:disable:next line_length
|
|
123
122
|
logger.info("Native version delay (value: \(value)) condition removed due to above limit at index \(index)")
|
|
@@ -151,59 +150,34 @@ public class DelayUpdateUtils {
|
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
public func setMultiDelay(delayConditions: String) -> Bool {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return true
|
|
159
|
-
} catch {
|
|
160
|
-
logger.error("Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
|
|
161
|
-
return false
|
|
162
|
-
}
|
|
153
|
+
UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
154
|
+
UserDefaults.standard.synchronize()
|
|
155
|
+
logger.info("Delay update saved")
|
|
156
|
+
return true
|
|
163
157
|
}
|
|
164
158
|
|
|
165
159
|
public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
logger.info("Background timestamp saved")
|
|
170
|
-
} catch {
|
|
171
|
-
logger.error("Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
|
|
172
|
-
}
|
|
160
|
+
UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
161
|
+
UserDefaults.standard.synchronize()
|
|
162
|
+
logger.info("Background timestamp saved")
|
|
173
163
|
}
|
|
174
164
|
|
|
175
165
|
public func unsetBackgroundTimestamp() {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
logger.info("Background timestamp removed")
|
|
180
|
-
} catch {
|
|
181
|
-
// swiftlint:disable:next line_length
|
|
182
|
-
logger.error("Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
|
|
183
|
-
}
|
|
166
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
167
|
+
UserDefaults.standard.synchronize()
|
|
168
|
+
logger.info("Background timestamp removed")
|
|
184
169
|
}
|
|
185
170
|
|
|
186
171
|
private func getBackgroundTimestamp() -> Int64 {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
let timestamp = UserDefaults.standard.object(forKey: key) as? Int64 ?? 0
|
|
190
|
-
return timestamp
|
|
191
|
-
} catch {
|
|
192
|
-
logger.error("Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
|
|
193
|
-
return 0
|
|
194
|
-
}
|
|
172
|
+
let key = DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY
|
|
173
|
+
return UserDefaults.standard.object(forKey: key) as? Int64 ?? 0
|
|
195
174
|
}
|
|
196
175
|
|
|
197
176
|
public func cancelDelay(source: String) -> Bool {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return true
|
|
203
|
-
} catch {
|
|
204
|
-
logger.error("Failed to cancel update delay: \(error)")
|
|
205
|
-
return false
|
|
206
|
-
}
|
|
177
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
178
|
+
UserDefaults.standard.synchronize()
|
|
179
|
+
logger.info("All delays canceled from \(source)")
|
|
180
|
+
return true
|
|
207
181
|
}
|
|
208
182
|
|
|
209
183
|
// MARK: - Helper methods
|
package/src/ios/Logger.swift
CHANGED
|
@@ -3,6 +3,8 @@ import os.log
|
|
|
3
3
|
import WebKit
|
|
4
4
|
|
|
5
5
|
public class Logger {
|
|
6
|
+
static let maxWebViewLogPayloadBytes = 4096
|
|
7
|
+
|
|
6
8
|
public enum LogLevel: Int {
|
|
7
9
|
case silent = 0
|
|
8
10
|
case error
|
|
@@ -173,20 +175,94 @@ public class Logger {
|
|
|
173
175
|
if let label = _labels[level] {
|
|
174
176
|
log(atLevel: level, label: label, tag: tag, message: message)
|
|
175
177
|
if let webView = self.webView {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let
|
|
179
|
-
|
|
178
|
+
let logTag = self.tag
|
|
179
|
+
DispatchQueue.main.async { [weak self] in
|
|
180
|
+
guard let self else { return }
|
|
181
|
+
if let script = self.buildWebViewConsoleScript(level: level, label: label, tag: logTag, message: message) {
|
|
182
|
+
webView.evaluateJavaScript(script, completionHandler: nil)
|
|
183
|
+
}
|
|
180
184
|
}
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
188
|
|
|
185
|
-
|
|
189
|
+
func consoleMethodName(for level: LogLevel) -> String? {
|
|
190
|
+
switch level {
|
|
191
|
+
case .error:
|
|
192
|
+
return "error"
|
|
193
|
+
case .warn:
|
|
194
|
+
return "warn"
|
|
195
|
+
case .info:
|
|
196
|
+
return "info"
|
|
197
|
+
case .debug:
|
|
198
|
+
return "debug"
|
|
199
|
+
case .silent:
|
|
200
|
+
return nil
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func utf8SequenceLength(_ lead: UInt8) -> Int {
|
|
205
|
+
if (lead & 0x80) == 0 {
|
|
206
|
+
return 1
|
|
207
|
+
}
|
|
208
|
+
if (lead & 0xE0) == 0xC0 {
|
|
209
|
+
return 2
|
|
210
|
+
}
|
|
211
|
+
if (lead & 0xF0) == 0xE0 {
|
|
212
|
+
return 3
|
|
213
|
+
}
|
|
214
|
+
if (lead & 0xF8) == 0xF0 {
|
|
215
|
+
return 4
|
|
216
|
+
}
|
|
217
|
+
return 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func capWebViewLogPayload(_ payload: String) -> String {
|
|
221
|
+
let suffix = "..."
|
|
222
|
+
let maxPayloadBytes = Logger.maxWebViewLogPayloadBytes
|
|
223
|
+
let payloadBytes = Array(payload.utf8.prefix(maxPayloadBytes + 1))
|
|
224
|
+
if payloadBytes.count <= maxPayloadBytes {
|
|
225
|
+
return payload
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
var end = maxPayloadBytes
|
|
229
|
+
while end > 0 && (payloadBytes[end - 1] & 0xC0) == 0x80 {
|
|
230
|
+
end -= 1
|
|
231
|
+
}
|
|
232
|
+
while end > 0 {
|
|
233
|
+
let leadIndex = end - 1
|
|
234
|
+
let lead = payloadBytes[leadIndex]
|
|
235
|
+
let seqLength = utf8SequenceLength(lead)
|
|
236
|
+
if seqLength == 0 {
|
|
237
|
+
end -= 1
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
if leadIndex + seqLength <= maxPayloadBytes {
|
|
241
|
+
end = leadIndex + seqLength
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
end -= 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return String(decoding: payloadBytes[0..<end], as: UTF8.self) + suffix
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func buildWebViewConsoleScript(level: LogLevel, label: String, tag: String, message: String) -> String? {
|
|
251
|
+
guard let consoleMethod = consoleMethodName(for: level) else {
|
|
252
|
+
return nil
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let payload = capWebViewLogPayload("\(label) \(tag) : \(message)")
|
|
256
|
+
return "console.\(consoleMethod)(\(toJSStringLiteral(payload)))"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func toJSStringLiteral(_ value: String) -> String {
|
|
186
260
|
// Prefer JSON encoding to produce a valid JS string literal
|
|
187
261
|
if let data = try? JSONEncoder().encode(value),
|
|
188
262
|
let encoded = String(data: data, encoding: .utf8) {
|
|
189
263
|
return encoded
|
|
264
|
+
.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
|
|
265
|
+
.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
|
|
190
266
|
}
|
|
191
267
|
|
|
192
268
|
// Fallback manual escaping (unlikely to be used)
|