@capgo/capacitor-updater 6.30.0 → 6.35.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 +100 -12
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +60 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +76 -38
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +49 -112
- package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipherV2.java → CryptoCipher.java} +80 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +86 -75
- package/dist/docs.json +105 -8
- package/dist/esm/definitions.d.ts +97 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +37 -10
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +33 -13
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +80 -89
- package/ios/Sources/CapacitorUpdaterPlugin/{CryptoCipherV2.swift → CryptoCipher.swift} +49 -3
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +4 -0
- package/package.json +2 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV1.java +0 -222
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift +0 -245
|
@@ -16,21 +16,25 @@ import Foundation
|
|
|
16
16
|
private let version: String
|
|
17
17
|
private let checksum: String
|
|
18
18
|
private let status: BundleStatus
|
|
19
|
+
private let link: String?
|
|
20
|
+
private let comment: String?
|
|
19
21
|
|
|
20
|
-
convenience init(id: String, version: String, status: BundleStatus, downloaded: Date, checksum: String) {
|
|
21
|
-
self.init(id: id, version: version, status: status, downloaded: downloaded.iso8601withFractionalSeconds, checksum: checksum)
|
|
22
|
+
convenience init(id: String, version: String, status: BundleStatus, downloaded: Date, checksum: String, link: String? = nil, comment: String? = nil) {
|
|
23
|
+
self.init(id: id, version: version, status: status, downloaded: downloaded.iso8601withFractionalSeconds, checksum: checksum, link: link, comment: comment)
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
init(id: String, version: String, status: BundleStatus, downloaded: String = BundleInfo.DOWNLOADED_BUILTIN, checksum: String) {
|
|
26
|
+
init(id: String, version: String, status: BundleStatus, downloaded: String = BundleInfo.DOWNLOADED_BUILTIN, checksum: String, link: String? = nil, comment: String? = nil) {
|
|
25
27
|
self.downloaded = downloaded.trim()
|
|
26
28
|
self.id = id
|
|
27
29
|
self.version = version
|
|
28
30
|
self.checksum = checksum
|
|
29
31
|
self.status = status
|
|
32
|
+
self.link = link
|
|
33
|
+
self.comment = comment
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
enum CodingKeys: String, CodingKey {
|
|
33
|
-
case downloaded, id, version, status, checksum
|
|
37
|
+
case downloaded, id, version, status, checksum, link, comment
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
public func isBuiltin() -> Bool {
|
|
@@ -62,11 +66,11 @@ import Foundation
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
public func setChecksum(checksum: String) -> BundleInfo {
|
|
65
|
-
return BundleInfo(id: self.id, version: self.version, status: self.status, downloaded: self.downloaded, checksum: checksum)
|
|
69
|
+
return BundleInfo(id: self.id, version: self.version, status: self.status, downloaded: self.downloaded, checksum: checksum, link: self.link, comment: self.comment)
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
public func setDownloaded(downloaded: Date) -> BundleInfo {
|
|
69
|
-
return BundleInfo(id: self.id, version: self.version, status: self.status, downloaded: downloaded, checksum: self.checksum)
|
|
73
|
+
return BundleInfo(id: self.id, version: self.version, status: self.status, downloaded: downloaded, checksum: self.checksum, link: self.link, comment: self.comment)
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
public func getId() -> String {
|
|
@@ -74,7 +78,7 @@ import Foundation
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
public func setId(id: String) -> BundleInfo {
|
|
77
|
-
return BundleInfo(id: id, version: self.version, status: self.status, downloaded: self.downloaded, checksum: self.checksum)
|
|
81
|
+
return BundleInfo(id: id, version: self.version, status: self.status, downloaded: self.downloaded, checksum: self.checksum, link: self.link, comment: self.comment)
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
public func getVersionName() -> String {
|
|
@@ -82,7 +86,7 @@ import Foundation
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
public func setVersionName(version: String) -> BundleInfo {
|
|
85
|
-
return BundleInfo(id: self.id, version: version, status: self.status, downloaded: self.downloaded, checksum: self.checksum)
|
|
89
|
+
return BundleInfo(id: self.id, version: version, status: self.status, downloaded: self.downloaded, checksum: self.checksum, link: self.link, comment: self.comment)
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
public func getStatus() -> String {
|
|
@@ -90,17 +94,40 @@ import Foundation
|
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
public func setStatus(status: String) -> BundleInfo {
|
|
93
|
-
return BundleInfo(id: self.id, version: self.version, status: BundleStatus(localizedString: status)!, downloaded: self.downloaded, checksum: self.checksum)
|
|
97
|
+
return BundleInfo(id: self.id, version: self.version, status: BundleStatus(localizedString: status)!, downloaded: self.downloaded, checksum: self.checksum, link: self.link, comment: self.comment)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public func getLink() -> String? {
|
|
101
|
+
return self.link
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public func setLink(link: String?) -> BundleInfo {
|
|
105
|
+
return BundleInfo(id: self.id, version: self.version, status: self.status, downloaded: self.downloaded, checksum: self.checksum, link: link, comment: self.comment)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public func getComment() -> String? {
|
|
109
|
+
return self.comment
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public func setComment(comment: String?) -> BundleInfo {
|
|
113
|
+
return BundleInfo(id: self.id, version: self.version, status: self.status, downloaded: self.downloaded, checksum: self.checksum, link: self.link, comment: comment)
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
public func toJSON() -> [String: String] {
|
|
97
|
-
|
|
117
|
+
var result: [String: String] = [
|
|
98
118
|
"id": self.getId(),
|
|
99
119
|
"version": self.getVersionName(),
|
|
100
120
|
"downloaded": self.getDownloaded(),
|
|
101
121
|
"checksum": self.getChecksum(),
|
|
102
122
|
"status": self.getStatus()
|
|
103
123
|
]
|
|
124
|
+
if let link = self.link {
|
|
125
|
+
result["link"] = link
|
|
126
|
+
}
|
|
127
|
+
if let comment = self.comment {
|
|
128
|
+
result["comment"] = comment
|
|
129
|
+
}
|
|
130
|
+
return result
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
public static func == (lhs: BundleInfo, rhs: BundleInfo) -> Bool {
|
|
@@ -54,7 +54,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
54
54
|
CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
|
|
55
55
|
]
|
|
56
56
|
public var implementation = CapgoUpdater()
|
|
57
|
-
private let pluginVersion: String = "6.
|
|
57
|
+
private let pluginVersion: String = "6.35.0"
|
|
58
58
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
59
59
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
60
60
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -63,6 +63,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
63
63
|
private let updateUrlDefaultsKey = "CapacitorUpdater.updateUrl"
|
|
64
64
|
private let statsUrlDefaultsKey = "CapacitorUpdater.statsUrl"
|
|
65
65
|
private let channelUrlDefaultsKey = "CapacitorUpdater.channelUrl"
|
|
66
|
+
private let defaultChannelDefaultsKey = "CapacitorUpdater.defaultChannel"
|
|
66
67
|
private let lastFailedBundleDefaultsKey = "CapacitorUpdater.lastFailedBundle"
|
|
67
68
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
68
69
|
private var updateUrl = ""
|
|
@@ -86,6 +87,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
86
87
|
private var autoSplashscreenTimedOut = false
|
|
87
88
|
private var autoDeleteFailed = false
|
|
88
89
|
private var autoDeletePrevious = false
|
|
90
|
+
private var allowSetDefaultChannel = true
|
|
89
91
|
private var keepUrlPathAfterReload = false
|
|
90
92
|
private var backgroundWork: DispatchWorkItem?
|
|
91
93
|
private var taskRunning = false
|
|
@@ -117,6 +119,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
117
119
|
// Use DeviceIdHelper to get or create device ID that persists across reinstalls
|
|
118
120
|
self.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId()
|
|
119
121
|
persistCustomId = getConfig().getBoolean("persistCustomId", false)
|
|
122
|
+
allowSetDefaultChannel = getConfig().getBoolean("allowSetDefaultChannel", true)
|
|
120
123
|
if persistCustomId {
|
|
121
124
|
let storedCustomId = UserDefaults.standard.string(forKey: customIdDefaultsKey) ?? ""
|
|
122
125
|
if !storedCustomId.isEmpty {
|
|
@@ -203,9 +206,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
203
206
|
|
|
204
207
|
// Set logger for shared classes
|
|
205
208
|
implementation.setLogger(logger)
|
|
206
|
-
|
|
207
|
-
CryptoCipherV1.setLogger(logger)
|
|
208
|
-
CryptoCipherV2.setLogger(logger)
|
|
209
|
+
CryptoCipher.setLogger(logger)
|
|
209
210
|
|
|
210
211
|
// Initialize DelayUpdateUtils
|
|
211
212
|
self.delayUpdateUtils = DelayUpdateUtils(currentVersionNative: currentVersionNative, logger: logger)
|
|
@@ -230,7 +231,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
230
231
|
logger.info("Loaded persisted channelUrl")
|
|
231
232
|
}
|
|
232
233
|
}
|
|
233
|
-
|
|
234
|
+
|
|
235
|
+
// Load defaultChannel: first try from persistent storage (set via setChannel), then fall back to config
|
|
236
|
+
if let storedDefaultChannel = UserDefaults.standard.object(forKey: defaultChannelDefaultsKey) as? String {
|
|
237
|
+
implementation.defaultChannel = storedDefaultChannel
|
|
238
|
+
logger.info("Loaded persisted defaultChannel from setChannel()")
|
|
239
|
+
} else {
|
|
240
|
+
implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
|
|
241
|
+
}
|
|
234
242
|
self.implementation.autoReset()
|
|
235
243
|
|
|
236
244
|
// Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
|
|
@@ -354,7 +362,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
354
362
|
}
|
|
355
363
|
|
|
356
364
|
private func cleanupObsoleteVersions() {
|
|
357
|
-
let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? "0"
|
|
365
|
+
let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
358
366
|
if previous != "0" && self.currentBuildVersion != previous {
|
|
359
367
|
_ = self._reset(toLastSuccessful: false)
|
|
360
368
|
let res = implementation.list()
|
|
@@ -507,7 +515,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
507
515
|
throw ObjectSavableError.checksum
|
|
508
516
|
}
|
|
509
517
|
|
|
510
|
-
checksum = try
|
|
518
|
+
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
519
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
520
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
511
521
|
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
512
522
|
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
513
523
|
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
@@ -702,7 +712,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
702
712
|
@objc func unsetChannel(_ call: CAPPluginCall) {
|
|
703
713
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate", false)
|
|
704
714
|
DispatchQueue.global(qos: .background).async {
|
|
705
|
-
let
|
|
715
|
+
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
716
|
+
let res = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
706
717
|
if res.error != "" {
|
|
707
718
|
call.reject(res.error, "UNSETCHANNEL_FAILED", nil, [
|
|
708
719
|
"message": res.error,
|
|
@@ -729,11 +740,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
729
740
|
}
|
|
730
741
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate") ?? false
|
|
731
742
|
DispatchQueue.global(qos: .background).async {
|
|
732
|
-
let res = self.implementation.setChannel(channel: channel)
|
|
743
|
+
let res = self.implementation.setChannel(channel: channel, defaultChannelKey: self.defaultChannelDefaultsKey, allowSetDefaultChannel: self.allowSetDefaultChannel)
|
|
733
744
|
if res.error != "" {
|
|
745
|
+
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
746
|
+
if res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed") {
|
|
747
|
+
self.notifyListeners("channelPrivate", data: [
|
|
748
|
+
"channel": channel,
|
|
749
|
+
"message": res.error
|
|
750
|
+
])
|
|
751
|
+
}
|
|
734
752
|
call.reject(res.error, "SETCHANNEL_FAILED", nil, [
|
|
735
753
|
"message": res.error,
|
|
736
|
-
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
754
|
+
"error": res.error.contains("Channel URL") ? "missing_config" : (res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed")) ? "channel_private" : "request_failed"
|
|
737
755
|
])
|
|
738
756
|
} else {
|
|
739
757
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
@@ -1323,9 +1341,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1323
1341
|
}
|
|
1324
1342
|
}
|
|
1325
1343
|
if res.manifest != nil {
|
|
1326
|
-
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey)
|
|
1344
|
+
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
1327
1345
|
} else {
|
|
1328
|
-
nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey)
|
|
1346
|
+
nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
1329
1347
|
}
|
|
1330
1348
|
}
|
|
1331
1349
|
guard let next = nextImpl else {
|
|
@@ -1338,7 +1356,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1338
1356
|
self.endBackGroundTaskWithNotif(msg: "Latest version is in error state. Aborting update.", latestVersionName: latestVersionName, current: current)
|
|
1339
1357
|
return
|
|
1340
1358
|
}
|
|
1341
|
-
res.checksum = try
|
|
1359
|
+
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
1360
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
1361
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
|
|
1342
1362
|
if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
|
|
1343
1363
|
self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
|
|
1344
1364
|
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
@@ -374,6 +374,12 @@ import UIKit
|
|
|
374
374
|
if let manifest = response.value?.manifest {
|
|
375
375
|
latest.manifest = manifest
|
|
376
376
|
}
|
|
377
|
+
if let link = response.value?.link {
|
|
378
|
+
latest.link = link
|
|
379
|
+
}
|
|
380
|
+
if let comment = response.value?.comment {
|
|
381
|
+
latest.comment = comment
|
|
382
|
+
}
|
|
377
383
|
case let .failure(error):
|
|
378
384
|
self.logger.error("Error getting Latest \(response.value.debugDescription) \(error)")
|
|
379
385
|
latest.message = "Error getting Latest \(String(describing: response.value))"
|
|
@@ -402,11 +408,11 @@ import UIKit
|
|
|
402
408
|
private var tempData = Data()
|
|
403
409
|
|
|
404
410
|
private func verifyChecksum(file: URL, expectedHash: String) -> Bool {
|
|
405
|
-
let actualHash =
|
|
411
|
+
let actualHash = CryptoCipher.calcChecksum(filePath: file)
|
|
406
412
|
return actualHash == expectedHash
|
|
407
413
|
}
|
|
408
414
|
|
|
409
|
-
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String) throws -> BundleInfo {
|
|
415
|
+
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
410
416
|
let id = self.randomString(length: 10)
|
|
411
417
|
logger.info("downloadManifest start \(id)")
|
|
412
418
|
let destFolder = self.getBundleDirectory(id: id)
|
|
@@ -416,7 +422,7 @@ import UIKit
|
|
|
416
422
|
try FileManager.default.createDirectory(at: destFolder, withIntermediateDirectories: true, attributes: nil)
|
|
417
423
|
|
|
418
424
|
// Create and save BundleInfo before starting the download process
|
|
419
|
-
let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "")
|
|
425
|
+
let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "", link: link, comment: comment)
|
|
420
426
|
self.saveBundleInfo(id: id, bundle: bundleInfo)
|
|
421
427
|
|
|
422
428
|
// Send stats for manifest download start
|
|
@@ -441,20 +447,26 @@ import UIKit
|
|
|
441
447
|
if !self.hasOldPrivateKeyPropertyInConfig && !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
442
448
|
// V2 Encryption (publicKey)
|
|
443
449
|
do {
|
|
444
|
-
fileHash = try
|
|
450
|
+
fileHash = try CryptoCipher.decryptChecksum(checksum: fileHash, publicKey: self.publicKey)
|
|
445
451
|
} catch {
|
|
446
452
|
downloadError = error
|
|
447
|
-
logger.error("
|
|
453
|
+
logger.error("CryptoCipher.decryptChecksum error \(id) \(fileName) error: \(error)")
|
|
448
454
|
}
|
|
449
455
|
} else if self.hasOldPrivateKeyPropertyInConfig {
|
|
450
456
|
// V1 Encryption (privateKey) - deprecated but supported
|
|
451
457
|
// V1 doesn't decrypt checksum, uses different method
|
|
452
458
|
}
|
|
453
459
|
|
|
460
|
+
// Check if file has .br extension for Brotli decompression
|
|
454
461
|
let fileNameWithoutPath = (fileName as NSString).lastPathComponent
|
|
455
462
|
let cacheFileName = "\(fileHash)_\(fileNameWithoutPath)"
|
|
456
463
|
let cacheFilePath = cacheFolder.appendingPathComponent(cacheFileName)
|
|
457
|
-
|
|
464
|
+
|
|
465
|
+
// Check if file is Brotli compressed and remove .br extension from destination
|
|
466
|
+
let isBrotli = fileName.hasSuffix(".br")
|
|
467
|
+
let destFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
468
|
+
|
|
469
|
+
let destFilePath = destFolder.appendingPathComponent(destFileName)
|
|
458
470
|
let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
|
|
459
471
|
|
|
460
472
|
// Create necessary subdirectories in the destination folder
|
|
@@ -463,16 +475,26 @@ import UIKit
|
|
|
463
475
|
dispatchGroup.enter()
|
|
464
476
|
|
|
465
477
|
if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
478
|
+
do {
|
|
479
|
+
try FileManager.default.copyItem(at: builtinFilePath, to: destFilePath)
|
|
480
|
+
logger.info("downloadManifest \(fileName) using builtin file \(id)")
|
|
481
|
+
completedFiles += 1
|
|
482
|
+
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
483
|
+
} catch {
|
|
484
|
+
downloadError = error
|
|
485
|
+
logger.error("Failed to copy builtin file \(fileName): \(error.localizedDescription)")
|
|
486
|
+
}
|
|
470
487
|
dispatchGroup.leave()
|
|
471
488
|
} else if FileManager.default.fileExists(atPath: cacheFilePath.path) && verifyChecksum(file: cacheFilePath, expectedHash: fileHash) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
489
|
+
do {
|
|
490
|
+
try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
|
|
491
|
+
logger.info("downloadManifest \(fileName) copy from cache \(id)")
|
|
492
|
+
completedFiles += 1
|
|
493
|
+
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
494
|
+
} catch {
|
|
495
|
+
downloadError = error
|
|
496
|
+
logger.error("Failed to copy cached file \(fileName): \(error.localizedDescription)")
|
|
497
|
+
}
|
|
476
498
|
dispatchGroup.leave()
|
|
477
499
|
} else {
|
|
478
500
|
// File not in cache, download, decompress, and save to both cache and destination
|
|
@@ -499,35 +521,22 @@ import UIKit
|
|
|
499
521
|
let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
|
|
500
522
|
try finalData.write(to: tempFile)
|
|
501
523
|
do {
|
|
502
|
-
try
|
|
524
|
+
try CryptoCipher.decryptFile(filePath: tempFile, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
|
|
503
525
|
} catch {
|
|
504
526
|
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
505
527
|
throw error
|
|
506
528
|
}
|
|
507
529
|
finalData = try Data(contentsOf: tempFile)
|
|
508
530
|
} else if self.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty {
|
|
509
|
-
// V1 Encryption (privateKey) - deprecated
|
|
510
|
-
|
|
511
|
-
try finalData.write(to: tempFile)
|
|
512
|
-
do {
|
|
513
|
-
try CryptoCipherV1.decryptFile(filePath: tempFile, privateKey: self.privateKey, sessionKey: sessionKey, version: version)
|
|
514
|
-
} catch {
|
|
515
|
-
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
516
|
-
throw error
|
|
517
|
-
}
|
|
518
|
-
finalData = try Data(contentsOf: tempFile)
|
|
519
|
-
try FileManager.default.removeItem(at: tempFile)
|
|
531
|
+
// V1 Encryption (privateKey) - deprecated not supported
|
|
532
|
+
throw NSError(domain: "DeprecatedEncryptionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "V1 Encryption with privateKey is deprecated and not supported in this download method for file \(fileName) at url \(downloadUrl)"])
|
|
520
533
|
}
|
|
521
534
|
|
|
522
|
-
//
|
|
523
|
-
let isBrotli = fileName.hasSuffix(".br")
|
|
524
|
-
let finalFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
525
|
-
let destFilePath = destFolder.appendingPathComponent(finalFileName)
|
|
526
|
-
|
|
535
|
+
// Use the isBrotli and destFilePath already computed above
|
|
527
536
|
if isBrotli {
|
|
528
537
|
// Decompress the Brotli data
|
|
529
538
|
guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
|
|
530
|
-
self.sendStats(action: "download_manifest_brotli_fail", versionName: "\(version):\(
|
|
539
|
+
self.sendStats(action: "download_manifest_brotli_fail", versionName: "\(version):\(destFileName)")
|
|
531
540
|
throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
|
|
532
541
|
}
|
|
533
542
|
finalData = decompressedData
|
|
@@ -536,9 +545,13 @@ import UIKit
|
|
|
536
545
|
try finalData.write(to: destFilePath)
|
|
537
546
|
if !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
538
547
|
// assume that calcChecksum != null
|
|
539
|
-
let calculatedChecksum =
|
|
548
|
+
let calculatedChecksum = CryptoCipher.calcChecksum(filePath: destFilePath)
|
|
549
|
+
CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
|
|
550
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
|
|
540
551
|
if calculatedChecksum != fileHash {
|
|
541
|
-
|
|
552
|
+
// Delete the corrupt file before throwing error
|
|
553
|
+
try? FileManager.default.removeItem(at: destFilePath)
|
|
554
|
+
self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
|
|
542
555
|
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
|
|
543
556
|
}
|
|
544
557
|
}
|
|
@@ -696,7 +709,7 @@ import UIKit
|
|
|
696
709
|
return status == COMPRESSION_STATUS_END ? decompressedData : nil
|
|
697
710
|
}
|
|
698
711
|
|
|
699
|
-
public func download(url: URL, version: String, sessionKey: String) throws -> BundleInfo {
|
|
712
|
+
public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
700
713
|
let id: String = self.randomString(length: 10)
|
|
701
714
|
let semaphore = DispatchSemaphore(value: 0)
|
|
702
715
|
if version != getLocalUpdateVersion() {
|
|
@@ -763,7 +776,7 @@ import UIKit
|
|
|
763
776
|
semaphore.signal()
|
|
764
777
|
}
|
|
765
778
|
}
|
|
766
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum))
|
|
779
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
767
780
|
let reachabilityManager = NetworkReachabilityManager()
|
|
768
781
|
reachabilityManager?.startListening { status in
|
|
769
782
|
switch status {
|
|
@@ -781,7 +794,7 @@ import UIKit
|
|
|
781
794
|
|
|
782
795
|
if mainError != nil {
|
|
783
796
|
logger.error("Failed to download: \(String(describing: mainError))")
|
|
784
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
797
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
785
798
|
throw mainError!
|
|
786
799
|
}
|
|
787
800
|
|
|
@@ -789,27 +802,28 @@ import UIKit
|
|
|
789
802
|
do {
|
|
790
803
|
if !self.hasOldPrivateKeyPropertyInConfig {
|
|
791
804
|
// V2 Encryption (publicKey)
|
|
792
|
-
try
|
|
805
|
+
try CryptoCipher.decryptFile(filePath: tempDataPath, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
|
|
793
806
|
} else {
|
|
794
807
|
// V1 Encryption (privateKey) - deprecated but supported
|
|
795
|
-
try
|
|
808
|
+
try CryptoCipher.decryptFile(filePath: tempDataPath, privateKey: self.privateKey, sessionKey: sessionKey, version: version)
|
|
796
809
|
}
|
|
797
810
|
try FileManager.default.moveItem(at: tempDataPath, to: finalPath)
|
|
798
811
|
} catch {
|
|
799
812
|
logger.error("Failed decrypt file : \(error)")
|
|
800
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
813
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
801
814
|
cleanDownloadData()
|
|
802
815
|
throw error
|
|
803
816
|
}
|
|
804
817
|
|
|
805
818
|
do {
|
|
806
|
-
checksum =
|
|
819
|
+
checksum = CryptoCipher.calcChecksum(filePath: finalPath)
|
|
820
|
+
CryptoCipher.logChecksumInfo(label: "Calculated bundle checksum", hexChecksum: checksum)
|
|
807
821
|
logger.info("Downloading: 80% (unzipping)")
|
|
808
822
|
try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
|
|
809
823
|
|
|
810
824
|
} catch {
|
|
811
825
|
logger.error("Failed to unzip file: \(error)")
|
|
812
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
826
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
813
827
|
// Best-effort cleanup of the decrypted zip file when unzip fails
|
|
814
828
|
do {
|
|
815
829
|
if FileManager.default.fileExists(atPath: finalPath.path) {
|
|
@@ -824,7 +838,7 @@ import UIKit
|
|
|
824
838
|
|
|
825
839
|
self.notifyDownload(id: id, percent: 90)
|
|
826
840
|
logger.info("Downloading: 90% (wrapping up)")
|
|
827
|
-
let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
|
|
841
|
+
let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum, link: link, comment: comment)
|
|
828
842
|
self.saveBundleInfo(id: id, bundle: info)
|
|
829
843
|
self.cleanDownloadData()
|
|
830
844
|
|
|
@@ -1131,60 +1145,31 @@ import UIKit
|
|
|
1131
1145
|
self.setBundleStatus(id: bundle.getId(), status: BundleStatus.ERROR)
|
|
1132
1146
|
}
|
|
1133
1147
|
|
|
1134
|
-
func unsetChannel() -> SetChannel {
|
|
1148
|
+
func unsetChannel(defaultChannelKey: String, configDefaultChannel: String) -> SetChannel {
|
|
1135
1149
|
let setChannel: SetChannel = SetChannel()
|
|
1136
1150
|
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
return setChannel
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
if (self.channelUrl ).isEmpty {
|
|
1146
|
-
logger.error("Channel URL is not set")
|
|
1147
|
-
setChannel.message = "Channel URL is not set"
|
|
1148
|
-
setChannel.error = "missing_config"
|
|
1149
|
-
return setChannel
|
|
1150
|
-
}
|
|
1151
|
-
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
|
1152
|
-
let parameters: InfoObject = self.createInfoObject()
|
|
1153
|
-
|
|
1154
|
-
let request = alamofireSession.request(self.channelUrl, method: .delete, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
|
|
1155
|
-
|
|
1156
|
-
request.validate().responseDecodable(of: SetChannelDec.self) { response in
|
|
1157
|
-
// Check for 429 rate limit
|
|
1158
|
-
if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
|
|
1159
|
-
setChannel.message = "Rate limit exceeded"
|
|
1160
|
-
setChannel.error = "rate_limit_exceeded"
|
|
1161
|
-
semaphore.signal()
|
|
1162
|
-
return
|
|
1163
|
-
}
|
|
1151
|
+
// Clear persisted defaultChannel and revert to config value
|
|
1152
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelKey)
|
|
1153
|
+
UserDefaults.standard.synchronize()
|
|
1154
|
+
self.defaultChannel = configDefaultChannel
|
|
1155
|
+
self.logger.info("Persisted defaultChannel cleared, reverted to config value: \(configDefaultChannel)")
|
|
1164
1156
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if let responseValue = response.value {
|
|
1168
|
-
if let error = responseValue.error {
|
|
1169
|
-
setChannel.error = error
|
|
1170
|
-
} else {
|
|
1171
|
-
setChannel.status = responseValue.status ?? ""
|
|
1172
|
-
setChannel.message = responseValue.message ?? ""
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
case let .failure(error):
|
|
1176
|
-
self.logger.error("Error unset Channel \(error)")
|
|
1177
|
-
setChannel.error = "Request failed: \(error.localizedDescription)"
|
|
1178
|
-
}
|
|
1179
|
-
semaphore.signal()
|
|
1180
|
-
}
|
|
1181
|
-
semaphore.wait()
|
|
1157
|
+
setChannel.status = "ok"
|
|
1158
|
+
setChannel.message = "Channel override removed"
|
|
1182
1159
|
return setChannel
|
|
1183
1160
|
}
|
|
1184
1161
|
|
|
1185
|
-
func setChannel(channel: String) -> SetChannel {
|
|
1162
|
+
func setChannel(channel: String, defaultChannelKey: String, allowSetDefaultChannel: Bool) -> SetChannel {
|
|
1186
1163
|
let setChannel: SetChannel = SetChannel()
|
|
1187
1164
|
|
|
1165
|
+
// Check if setting defaultChannel is allowed
|
|
1166
|
+
if !allowSetDefaultChannel {
|
|
1167
|
+
logger.error("setChannel is disabled by allowSetDefaultChannel config")
|
|
1168
|
+
setChannel.message = "setChannel is disabled by configuration"
|
|
1169
|
+
setChannel.error = "disabled_by_config"
|
|
1170
|
+
return setChannel
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1188
1173
|
// Check if rate limit was exceeded
|
|
1189
1174
|
if CapgoUpdater.rateLimitExceeded {
|
|
1190
1175
|
logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.")
|
|
@@ -1220,6 +1205,12 @@ import UIKit
|
|
|
1220
1205
|
if let error = responseValue.error {
|
|
1221
1206
|
setChannel.error = error
|
|
1222
1207
|
} else {
|
|
1208
|
+
// Success - persist defaultChannel
|
|
1209
|
+
self.defaultChannel = channel
|
|
1210
|
+
UserDefaults.standard.set(channel, forKey: defaultChannelKey)
|
|
1211
|
+
UserDefaults.standard.synchronize()
|
|
1212
|
+
self.logger.info("defaultChannel persisted locally: \(channel)")
|
|
1213
|
+
|
|
1223
1214
|
setChannel.status = responseValue.status ?? ""
|
|
1224
1215
|
setChannel.message = responseValue.message ?? ""
|
|
1225
1216
|
}
|
|
@@ -8,8 +8,7 @@ import Foundation
|
|
|
8
8
|
import CryptoKit
|
|
9
9
|
import BigInt
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
public struct CryptoCipherV2 {
|
|
11
|
+
public struct CryptoCipher {
|
|
13
12
|
private static var logger: Logger!
|
|
14
13
|
|
|
15
14
|
public static func setLogger(_ logger: Logger) {
|
|
@@ -42,6 +41,7 @@ public struct CryptoCipherV2 {
|
|
|
42
41
|
// Determine if input is hex or base64 encoded
|
|
43
42
|
// Hex strings only contain 0-9 and a-f, while base64 contains other characters
|
|
44
43
|
let checksumBytes: Data
|
|
44
|
+
let detectedFormat: String
|
|
45
45
|
if isHexString(checksum) {
|
|
46
46
|
// Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
|
|
47
47
|
guard let hexData = hexStringToData(checksum) else {
|
|
@@ -49,6 +49,7 @@ public struct CryptoCipherV2 {
|
|
|
49
49
|
throw CustomError.cannotDecode
|
|
50
50
|
}
|
|
51
51
|
checksumBytes = hexData
|
|
52
|
+
detectedFormat = "hex"
|
|
52
53
|
} else {
|
|
53
54
|
// TODO: remove backwards compatibility
|
|
54
55
|
// Base64 encoded (old format for backwards compatibility)
|
|
@@ -57,7 +58,9 @@ public struct CryptoCipherV2 {
|
|
|
57
58
|
throw CustomError.cannotDecode
|
|
58
59
|
}
|
|
59
60
|
checksumBytes = base64Data
|
|
61
|
+
detectedFormat = "base64"
|
|
60
62
|
}
|
|
63
|
+
logger.debug("Received encrypted checksum format: \(detectedFormat) (length: \(checksum.count) chars, \(checksumBytes.count) bytes)")
|
|
61
64
|
|
|
62
65
|
if checksumBytes.isEmpty {
|
|
63
66
|
logger.error("Decoded checksum is empty")
|
|
@@ -75,12 +78,55 @@ public struct CryptoCipherV2 {
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
// Return as hex string to match calcChecksum output format
|
|
78
|
-
|
|
81
|
+
let result = decryptedChecksum.map { String(format: "%02x", $0) }.joined()
|
|
82
|
+
|
|
83
|
+
// Detect checksum algorithm based on length
|
|
84
|
+
let detectedAlgorithm: String
|
|
85
|
+
if decryptedChecksum.count == 32 {
|
|
86
|
+
detectedAlgorithm = "SHA-256"
|
|
87
|
+
} else if decryptedChecksum.count == 4 {
|
|
88
|
+
detectedAlgorithm = "CRC32 (deprecated)"
|
|
89
|
+
logger.error("CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums.")
|
|
90
|
+
} else {
|
|
91
|
+
detectedAlgorithm = "unknown (\(decryptedChecksum.count) bytes)"
|
|
92
|
+
logger.error("Unknown checksum algorithm detected with \(decryptedChecksum.count) bytes. Expected SHA-256 (32 bytes).")
|
|
93
|
+
}
|
|
94
|
+
logger.debug("Decrypted checksum: \(detectedAlgorithm) hex format (length: \(result.count) chars, \(decryptedChecksum.count) bytes)")
|
|
95
|
+
return result
|
|
79
96
|
} catch {
|
|
80
97
|
logger.error("decryptChecksum fail: \(error.localizedDescription)")
|
|
81
98
|
throw CustomError.cannotDecode
|
|
82
99
|
}
|
|
83
100
|
}
|
|
101
|
+
|
|
102
|
+
/// Detect checksum algorithm based on hex string length.
|
|
103
|
+
/// SHA-256 = 64 hex chars (32 bytes)
|
|
104
|
+
/// CRC32 = 8 hex chars (4 bytes)
|
|
105
|
+
public static func detectChecksumAlgorithm(_ hexChecksum: String) -> String {
|
|
106
|
+
if hexChecksum.isEmpty {
|
|
107
|
+
return "empty"
|
|
108
|
+
}
|
|
109
|
+
let len = hexChecksum.count
|
|
110
|
+
if len == 64 {
|
|
111
|
+
return "SHA-256"
|
|
112
|
+
} else if len == 8 {
|
|
113
|
+
return "CRC32 (deprecated)"
|
|
114
|
+
} else {
|
|
115
|
+
return "unknown (\(len) hex chars)"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Log checksum info and warn if deprecated algorithm detected.
|
|
120
|
+
public static func logChecksumInfo(label: String, hexChecksum: String) {
|
|
121
|
+
let algorithm = detectChecksumAlgorithm(hexChecksum)
|
|
122
|
+
logger.debug("\(label): \(algorithm) hex format (length: \(hexChecksum.count) chars)")
|
|
123
|
+
if algorithm.contains("CRC32") {
|
|
124
|
+
logger.error("CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums.")
|
|
125
|
+
} else if algorithm.contains("unknown") {
|
|
126
|
+
logger.error("Unknown checksum algorithm detected. Expected SHA-256 (64 hex chars) but got \(hexChecksum.count) chars.")
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
84
130
|
public static func calcChecksum(filePath: URL) -> String {
|
|
85
131
|
let bufferSize = 1024 * 1024 * 5 // 5 MB
|
|
86
132
|
var sha256 = SHA256()
|