@capgo/capacitor-updater 6.14.26 → 6.14.29
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/CapgoCapacitorUpdater.podspec +3 -2
- package/Package.swift +2 -2
- package/README.md +341 -74
- package/android/build.gradle +20 -8
- package/android/proguard-rules.pro +22 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +52 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1196 -508
- package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +522 -154
- package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipher.java → CryptoCipherV1.java} +17 -9
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +15 -26
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +300 -119
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +63 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
- package/dist/docs.json +652 -63
- package/dist/esm/definitions.d.ts +265 -15
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +12 -1
- package/dist/esm/web.js +29 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +311 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +311 -2
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/AES.swift +6 -3
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1575 -0
- package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +365 -139
- package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift} +13 -6
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/CryptoCipherV2.swift +33 -27
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +47 -0
- package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/RSA.swift +1 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
- package/package.json +20 -16
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -1030
- /package/{LICENCE → LICENSE} +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BigInt.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift}
RENAMED
|
@@ -14,7 +14,8 @@ import Alamofire
|
|
|
14
14
|
import Compression
|
|
15
15
|
import UIKit
|
|
16
16
|
|
|
17
|
-
@objc public class
|
|
17
|
+
@objc public class CapgoUpdater: NSObject {
|
|
18
|
+
private var logger: Logger!
|
|
18
19
|
|
|
19
20
|
private let versionCode: String = Bundle.main.versionCode ?? ""
|
|
20
21
|
private let versionOs = UIDevice.current.systemVersion
|
|
@@ -29,7 +30,6 @@ import UIKit
|
|
|
29
30
|
// Add this line to declare cacheFolder
|
|
30
31
|
private let cacheFolder: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("capgo_downloads")
|
|
31
32
|
|
|
32
|
-
public static let TAG: String = "✨ Capacitor-updater:"
|
|
33
33
|
public let CAP_SERVER_PATH: String = "serverBasePath"
|
|
34
34
|
public var versionBuild: String = ""
|
|
35
35
|
public var customId: String = ""
|
|
@@ -44,12 +44,31 @@ import UIKit
|
|
|
44
44
|
public var publicKey: String = ""
|
|
45
45
|
public var hasOldPrivateKeyPropertyInConfig: Bool = false
|
|
46
46
|
|
|
47
|
+
// Flag to track if we received a 429 response - stops requests until app restart
|
|
48
|
+
private static var rateLimitExceeded = false
|
|
49
|
+
|
|
50
|
+
private var userAgent: String {
|
|
51
|
+
let safePluginVersion = PLUGIN_VERSION.isEmpty ? "unknown" : PLUGIN_VERSION
|
|
52
|
+
let safeAppId = appId.isEmpty ? "unknown" : appId
|
|
53
|
+
return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId)) ios/\(versionOs)"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private lazy var alamofireSession: Session = {
|
|
57
|
+
let configuration = URLSessionConfiguration.default
|
|
58
|
+
configuration.httpAdditionalHeaders = ["User-Agent": self.userAgent]
|
|
59
|
+
return Session(configuration: configuration)
|
|
60
|
+
}()
|
|
61
|
+
|
|
47
62
|
public var notifyDownloadRaw: (String, Int, Bool) -> Void = { _, _, _ in }
|
|
48
63
|
public func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false) {
|
|
49
64
|
notifyDownloadRaw(id, percent, ignoreMultipleOfTen)
|
|
50
65
|
}
|
|
51
66
|
public var notifyDownload: (String, Int) -> Void = { _, _ in }
|
|
52
67
|
|
|
68
|
+
public func setLogger(_ logger: Logger) {
|
|
69
|
+
self.logger = logger
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
|
|
54
73
|
return (percent * (max - min)) / 100 + min
|
|
55
74
|
}
|
|
@@ -71,6 +90,18 @@ import UIKit
|
|
|
71
90
|
return !self.isDevEnvironment && !self.isAppStoreReceiptSandbox() && !self.hasEmbeddedMobileProvision()
|
|
72
91
|
}
|
|
73
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Check if a 429 (Too Many Requests) response was received and set the flag
|
|
95
|
+
*/
|
|
96
|
+
private func checkAndHandleRateLimitResponse(statusCode: Int?) -> Bool {
|
|
97
|
+
if statusCode == 429 {
|
|
98
|
+
CapgoUpdater.rateLimitExceeded = true
|
|
99
|
+
logger.warn("Rate limit exceeded (429). Stopping all stats and channel requests until app restart.")
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
// MARK: Private
|
|
75
106
|
private func hasEmbeddedMobileProvision() -> Bool {
|
|
76
107
|
guard Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") == nil else {
|
|
@@ -110,7 +141,7 @@ import UIKit
|
|
|
110
141
|
do {
|
|
111
142
|
try FileManager.default.createDirectory(atPath: source.path, withIntermediateDirectories: true, attributes: nil)
|
|
112
143
|
} catch {
|
|
113
|
-
|
|
144
|
+
logger.error("Cannot createDirectory \(source.path)")
|
|
114
145
|
throw CustomError.cannotCreateDirectory
|
|
115
146
|
}
|
|
116
147
|
}
|
|
@@ -120,7 +151,7 @@ import UIKit
|
|
|
120
151
|
do {
|
|
121
152
|
try FileManager.default.removeItem(atPath: source.path)
|
|
122
153
|
} catch {
|
|
123
|
-
|
|
154
|
+
logger.error("File not removed. \(source.path)")
|
|
124
155
|
throw CustomError.cannotDeleteDirectory
|
|
125
156
|
}
|
|
126
157
|
}
|
|
@@ -137,14 +168,14 @@ import UIKit
|
|
|
137
168
|
return false
|
|
138
169
|
}
|
|
139
170
|
} catch {
|
|
140
|
-
|
|
171
|
+
logger.error("File not moved. source: \(source.path) dest: \(dest.path)")
|
|
141
172
|
throw CustomError.cannotUnflat
|
|
142
173
|
}
|
|
143
174
|
}
|
|
144
175
|
|
|
145
176
|
private func unzipProgressHandler(entry: String, zipInfo: unz_file_info, entryNumber: Int, total: Int, destUnZip: URL, id: String, unzipError: inout NSError?) {
|
|
146
177
|
if entry.contains("\\") {
|
|
147
|
-
|
|
178
|
+
logger.error("unzip: Windows path is not supported, please use unix path as required by zip RFC: \(entry)")
|
|
148
179
|
self.sendStats(action: "windows_path_fail")
|
|
149
180
|
}
|
|
150
181
|
|
|
@@ -227,7 +258,7 @@ import UIKit
|
|
|
227
258
|
try FileManager.default.removeItem(at: sourceZip)
|
|
228
259
|
}
|
|
229
260
|
} catch {
|
|
230
|
-
|
|
261
|
+
logger.error("Could not delete source zip at \(sourceZip.path): \(error)")
|
|
231
262
|
}
|
|
232
263
|
}
|
|
233
264
|
|
|
@@ -257,8 +288,8 @@ import UIKit
|
|
|
257
288
|
if let channel = channel {
|
|
258
289
|
parameters.defaultChannel = channel
|
|
259
290
|
}
|
|
260
|
-
|
|
261
|
-
let request =
|
|
291
|
+
logger.info("Auto-update parameters: \(parameters)")
|
|
292
|
+
let request = alamofireSession.request(url, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
|
|
262
293
|
|
|
263
294
|
request.validate().responseDecodable(of: AppVersionDec.self) { response in
|
|
264
295
|
switch response.result {
|
|
@@ -275,6 +306,9 @@ import UIKit
|
|
|
275
306
|
if let major = response.value?.major {
|
|
276
307
|
latest.major = major
|
|
277
308
|
}
|
|
309
|
+
if let breaking = response.value?.breaking {
|
|
310
|
+
latest.breaking = breaking
|
|
311
|
+
}
|
|
278
312
|
if let error = response.value?.error {
|
|
279
313
|
latest.error = error
|
|
280
314
|
}
|
|
@@ -291,7 +325,7 @@ import UIKit
|
|
|
291
325
|
latest.manifest = manifest
|
|
292
326
|
}
|
|
293
327
|
case let .failure(error):
|
|
294
|
-
|
|
328
|
+
self.logger.error("Error getting Latest \(response.value.debugDescription) \(error)")
|
|
295
329
|
latest.message = "Error getting Latest \(String(describing: response.value))"
|
|
296
330
|
latest.error = "response_error"
|
|
297
331
|
}
|
|
@@ -304,7 +338,7 @@ import UIKit
|
|
|
304
338
|
private func setCurrentBundle(bundle: String) {
|
|
305
339
|
UserDefaults.standard.set(bundle, forKey: self.CAP_SERVER_PATH)
|
|
306
340
|
UserDefaults.standard.synchronize()
|
|
307
|
-
|
|
341
|
+
logger.info("Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
|
|
308
342
|
}
|
|
309
343
|
|
|
310
344
|
private var tempDataPath: URL {
|
|
@@ -323,7 +357,7 @@ import UIKit
|
|
|
323
357
|
|
|
324
358
|
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String) throws -> BundleInfo {
|
|
325
359
|
let id = self.randomString(length: 10)
|
|
326
|
-
|
|
360
|
+
logger.info("downloadManifest start \(id)")
|
|
327
361
|
let destFolder = self.getBundleDirectory(id: id)
|
|
328
362
|
let builtinFolder = Bundle.main.bundleURL.appendingPathComponent("public")
|
|
329
363
|
|
|
@@ -351,12 +385,16 @@ import UIKit
|
|
|
351
385
|
}
|
|
352
386
|
|
|
353
387
|
if !self.hasOldPrivateKeyPropertyInConfig && !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
388
|
+
// V2 Encryption (publicKey)
|
|
354
389
|
do {
|
|
355
|
-
fileHash = try CryptoCipherV2.decryptChecksum(checksum: fileHash, publicKey: self.publicKey
|
|
390
|
+
fileHash = try CryptoCipherV2.decryptChecksum(checksum: fileHash, publicKey: self.publicKey)
|
|
356
391
|
} catch {
|
|
357
392
|
downloadError = error
|
|
358
|
-
|
|
393
|
+
logger.error("CryptoCipherV2.decryptChecksum error \(id) \(fileName) error: \(error)")
|
|
359
394
|
}
|
|
395
|
+
} else if self.hasOldPrivateKeyPropertyInConfig {
|
|
396
|
+
// V1 Encryption (privateKey) - deprecated but supported
|
|
397
|
+
// V1 doesn't decrypt checksum, uses different method
|
|
360
398
|
}
|
|
361
399
|
|
|
362
400
|
let fileNameWithoutPath = (fileName as NSString).lastPathComponent
|
|
@@ -372,19 +410,19 @@ import UIKit
|
|
|
372
410
|
|
|
373
411
|
if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
|
|
374
412
|
try FileManager.default.copyItem(at: builtinFilePath, to: destFilePath)
|
|
375
|
-
|
|
413
|
+
logger.info("downloadManifest \(fileName) using builtin file \(id)")
|
|
376
414
|
completedFiles += 1
|
|
377
415
|
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
378
416
|
dispatchGroup.leave()
|
|
379
417
|
} else if FileManager.default.fileExists(atPath: cacheFilePath.path) && verifyChecksum(file: cacheFilePath, expectedHash: fileHash) {
|
|
380
418
|
try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
|
|
381
|
-
|
|
419
|
+
logger.info("downloadManifest \(fileName) copy from cache \(id)")
|
|
382
420
|
completedFiles += 1
|
|
383
421
|
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
384
422
|
dispatchGroup.leave()
|
|
385
423
|
} else {
|
|
386
424
|
// File not in cache, download, decompress, and save to both cache and destination
|
|
387
|
-
|
|
425
|
+
self.alamofireSession.download(downloadUrl).responseData { response in
|
|
388
426
|
defer { dispatchGroup.leave() }
|
|
389
427
|
|
|
390
428
|
switch response.result {
|
|
@@ -399,9 +437,10 @@ import UIKit
|
|
|
399
437
|
}
|
|
400
438
|
}
|
|
401
439
|
|
|
402
|
-
// Add decryption step if
|
|
440
|
+
// Add decryption step if encryption keys are set
|
|
403
441
|
var finalData = data
|
|
404
|
-
if !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
442
|
+
if !self.hasOldPrivateKeyPropertyInConfig && !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
443
|
+
// V2 Encryption (publicKey)
|
|
405
444
|
let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
|
|
406
445
|
try finalData.write(to: tempFile)
|
|
407
446
|
do {
|
|
@@ -410,19 +449,36 @@ import UIKit
|
|
|
410
449
|
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
411
450
|
throw error
|
|
412
451
|
}
|
|
413
|
-
|
|
452
|
+
finalData = try Data(contentsOf: tempFile)
|
|
453
|
+
} else if self.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty {
|
|
454
|
+
// V1 Encryption (privateKey) - deprecated but supported
|
|
455
|
+
let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
|
|
456
|
+
try finalData.write(to: tempFile)
|
|
457
|
+
do {
|
|
458
|
+
try CryptoCipherV1.decryptFile(filePath: tempFile, privateKey: self.privateKey, sessionKey: sessionKey, version: version)
|
|
459
|
+
} catch {
|
|
460
|
+
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
461
|
+
throw error
|
|
462
|
+
}
|
|
414
463
|
finalData = try Data(contentsOf: tempFile)
|
|
415
464
|
try FileManager.default.removeItem(at: tempFile)
|
|
416
465
|
}
|
|
417
466
|
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
|
|
467
|
+
// Check if file has .br extension for Brotli decompression
|
|
468
|
+
let isBrotli = fileName.hasSuffix(".br")
|
|
469
|
+
let finalFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
470
|
+
let destFilePath = destFolder.appendingPathComponent(finalFileName)
|
|
471
|
+
|
|
472
|
+
if isBrotli {
|
|
473
|
+
// Decompress the Brotli data
|
|
474
|
+
guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
|
|
475
|
+
throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
|
|
476
|
+
}
|
|
477
|
+
finalData = decompressedData
|
|
421
478
|
}
|
|
422
|
-
finalData = decompressedData
|
|
423
479
|
|
|
424
480
|
try finalData.write(to: destFilePath)
|
|
425
|
-
if !self.
|
|
481
|
+
if !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
426
482
|
// assume that calcChecksum != null
|
|
427
483
|
let calculatedChecksum = CryptoCipherV2.calcChecksum(filePath: destFilePath)
|
|
428
484
|
if calculatedChecksum != fileHash {
|
|
@@ -435,13 +491,13 @@ import UIKit
|
|
|
435
491
|
|
|
436
492
|
completedFiles += 1
|
|
437
493
|
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
438
|
-
|
|
494
|
+
self.logger.info("downloadManifest \(id) \(fileName) downloaded\(isBrotli ? ", decompressed" : "")\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
|
|
439
495
|
} catch {
|
|
440
496
|
downloadError = error
|
|
441
|
-
|
|
497
|
+
self.logger.error("downloadManifest \(id) \(fileName) error: \(error.localizedDescription)")
|
|
442
498
|
}
|
|
443
499
|
case .failure(let error):
|
|
444
|
-
|
|
500
|
+
self.logger.error("downloadManifest \(id) \(fileName) download error: \(error.localizedDescription). Debug response: \(response.debugDescription).")
|
|
445
501
|
}
|
|
446
502
|
}
|
|
447
503
|
}
|
|
@@ -460,7 +516,7 @@ import UIKit
|
|
|
460
516
|
let updatedBundle = bundleInfo.setStatus(status: BundleStatus.PENDING.localizedString)
|
|
461
517
|
self.saveBundleInfo(id: id, bundle: updatedBundle)
|
|
462
518
|
|
|
463
|
-
|
|
519
|
+
logger.info("downloadManifest done \(id)")
|
|
464
520
|
return updatedBundle
|
|
465
521
|
}
|
|
466
522
|
|
|
@@ -501,7 +557,7 @@ import UIKit
|
|
|
501
557
|
var status = compression_stream_init(streamPointer, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
|
|
502
558
|
|
|
503
559
|
guard status != COMPRESSION_STATUS_ERROR else {
|
|
504
|
-
|
|
560
|
+
logger.error("Error: Failed to initialize Brotli stream for \(fileName). Status: \(status)")
|
|
505
561
|
return nil
|
|
506
562
|
}
|
|
507
563
|
|
|
@@ -523,7 +579,7 @@ import UIKit
|
|
|
523
579
|
if let baseAddress = rawBufferPointer.baseAddress {
|
|
524
580
|
streamPointer.pointee.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
|
|
525
581
|
} else {
|
|
526
|
-
|
|
582
|
+
logger.error("Error: Failed to get base address for \(fileName)")
|
|
527
583
|
status = COMPRESSION_STATUS_ERROR
|
|
528
584
|
return
|
|
529
585
|
}
|
|
@@ -533,7 +589,7 @@ import UIKit
|
|
|
533
589
|
if status == COMPRESSION_STATUS_ERROR {
|
|
534
590
|
let maxBytes = min(32, data.count)
|
|
535
591
|
let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
536
|
-
|
|
592
|
+
logger.error("Error: Brotli decompression failed for \(fileName). First \(maxBytes) bytes: \(hexDump)")
|
|
537
593
|
break
|
|
538
594
|
}
|
|
539
595
|
|
|
@@ -547,18 +603,18 @@ import UIKit
|
|
|
547
603
|
if status == COMPRESSION_STATUS_END {
|
|
548
604
|
break
|
|
549
605
|
} else if status == COMPRESSION_STATUS_ERROR {
|
|
550
|
-
|
|
606
|
+
logger.error("Error: Brotli process failed for \(fileName). Status: \(status)")
|
|
551
607
|
if let text = String(data: data, encoding: .utf8) {
|
|
552
608
|
let asciiCount = text.unicodeScalars.filter { $0.isASCII }.count
|
|
553
609
|
let totalCount = text.unicodeScalars.count
|
|
554
610
|
if totalCount > 0 && Double(asciiCount) / Double(totalCount) >= 0.8 {
|
|
555
|
-
|
|
611
|
+
logger.error("Error: Input appears to be plain text: \(text)")
|
|
556
612
|
}
|
|
557
613
|
}
|
|
558
614
|
|
|
559
615
|
let maxBytes = min(32, data.count)
|
|
560
616
|
let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
561
|
-
|
|
617
|
+
logger.error("Error: Raw data (\(fileName)): \(hexDump)")
|
|
562
618
|
|
|
563
619
|
return nil
|
|
564
620
|
}
|
|
@@ -569,7 +625,7 @@ import UIKit
|
|
|
569
625
|
}
|
|
570
626
|
|
|
571
627
|
if input.count == 0 {
|
|
572
|
-
|
|
628
|
+
logger.error("Error: Zero input size for \(fileName)")
|
|
573
629
|
break
|
|
574
630
|
}
|
|
575
631
|
}
|
|
@@ -598,11 +654,13 @@ import UIKit
|
|
|
598
654
|
let monitor = ClosureEventMonitor()
|
|
599
655
|
monitor.requestDidCompleteTaskWithError = { (_, _, error) in
|
|
600
656
|
if error != nil {
|
|
601
|
-
|
|
657
|
+
self.logger.error("Downloading failed - ClosureEventMonitor activated")
|
|
602
658
|
mainError = error as NSError?
|
|
603
659
|
}
|
|
604
660
|
}
|
|
605
|
-
let
|
|
661
|
+
let configuration = URLSessionConfiguration.default
|
|
662
|
+
configuration.httpAdditionalHeaders = ["User-Agent": self.userAgent]
|
|
663
|
+
let session = Session(configuration: configuration, eventMonitors: [monitor])
|
|
606
664
|
|
|
607
665
|
let request = session.streamRequest(url, headers: requestHeaders).validate().onHTTPResponse(perform: { response in
|
|
608
666
|
if let contentLength = response.headers.value(for: "Content-Length") {
|
|
@@ -629,11 +687,11 @@ import UIKit
|
|
|
629
687
|
}
|
|
630
688
|
|
|
631
689
|
} else {
|
|
632
|
-
|
|
690
|
+
self.logger.error("Download failed")
|
|
633
691
|
}
|
|
634
692
|
|
|
635
693
|
case .complete:
|
|
636
|
-
|
|
694
|
+
self.logger.info("Download complete, total received bytes: \(totalReceivedBytes)")
|
|
637
695
|
self.notifyDownload(id: id, percent: 70, ignoreMultipleOfTen: true)
|
|
638
696
|
semaphore.signal()
|
|
639
697
|
}
|
|
@@ -655,69 +713,68 @@ import UIKit
|
|
|
655
713
|
reachabilityManager?.stopListening()
|
|
656
714
|
|
|
657
715
|
if mainError != nil {
|
|
658
|
-
|
|
716
|
+
logger.error("Failed to download: \(String(describing: mainError))")
|
|
659
717
|
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
660
718
|
throw mainError!
|
|
661
719
|
}
|
|
662
720
|
|
|
663
721
|
let finalPath = tempDataPath.deletingLastPathComponent().appendingPathComponent("\(id)")
|
|
664
722
|
do {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
} catch {
|
|
673
|
-
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
674
|
-
throw error
|
|
723
|
+
if !self.hasOldPrivateKeyPropertyInConfig {
|
|
724
|
+
// V2 Encryption (publicKey)
|
|
725
|
+
try CryptoCipherV2.decryptFile(filePath: tempDataPath, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
|
|
726
|
+
} else {
|
|
727
|
+
// V1 Encryption (privateKey) - deprecated but supported
|
|
728
|
+
try CryptoCipherV1.decryptFile(filePath: tempDataPath, privateKey: self.privateKey, sessionKey: sessionKey, version: version)
|
|
675
729
|
}
|
|
676
730
|
try FileManager.default.moveItem(at: tempDataPath, to: finalPath)
|
|
677
731
|
} catch {
|
|
678
|
-
|
|
732
|
+
logger.error("Failed decrypt file : \(error)")
|
|
679
733
|
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
680
734
|
cleanDownloadData()
|
|
681
735
|
throw error
|
|
682
736
|
}
|
|
683
737
|
|
|
684
738
|
do {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
} else {
|
|
688
|
-
checksum = CryptoCipher.calcChecksum(filePath: finalPath)
|
|
689
|
-
}
|
|
690
|
-
print("\(CapacitorUpdater.TAG) Downloading: 80% (unzipping)")
|
|
739
|
+
checksum = CryptoCipherV2.calcChecksum(filePath: finalPath)
|
|
740
|
+
logger.info("Downloading: 80% (unzipping)")
|
|
691
741
|
try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
|
|
692
742
|
|
|
693
743
|
} catch {
|
|
694
|
-
|
|
744
|
+
logger.error("Failed to unzip file: \(error)")
|
|
695
745
|
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
746
|
+
// Best-effort cleanup of the decrypted zip file when unzip fails
|
|
747
|
+
do {
|
|
748
|
+
if FileManager.default.fileExists(atPath: finalPath.path) {
|
|
749
|
+
try FileManager.default.removeItem(at: finalPath)
|
|
750
|
+
}
|
|
751
|
+
} catch {
|
|
752
|
+
logger.error("Could not delete failed zip at \(finalPath.path): \(error)")
|
|
753
|
+
}
|
|
696
754
|
cleanDownloadData()
|
|
697
|
-
// todo: cleanup zip attempts
|
|
698
755
|
throw error
|
|
699
756
|
}
|
|
700
757
|
|
|
701
758
|
self.notifyDownload(id: id, percent: 90)
|
|
702
|
-
|
|
759
|
+
logger.info("Downloading: 90% (wrapping up)")
|
|
703
760
|
let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
|
|
704
761
|
self.saveBundleInfo(id: id, bundle: info)
|
|
705
762
|
self.cleanDownloadData()
|
|
706
763
|
self.notifyDownload(id: id, percent: 100)
|
|
707
|
-
|
|
764
|
+
logger.info("Downloading: 100% (complete)")
|
|
708
765
|
return info
|
|
709
766
|
}
|
|
710
767
|
private func ensureResumableFilesExist() {
|
|
711
768
|
let fileManager = FileManager.default
|
|
712
769
|
if !fileManager.fileExists(atPath: tempDataPath.path) {
|
|
713
770
|
if !fileManager.createFile(atPath: tempDataPath.path, contents: Data()) {
|
|
714
|
-
|
|
771
|
+
logger.error("Cannot ensure that a file at \(tempDataPath.path) exists")
|
|
715
772
|
}
|
|
716
773
|
}
|
|
717
774
|
|
|
718
775
|
if !fileManager.fileExists(atPath: updateInfo.path) {
|
|
719
776
|
if !fileManager.createFile(atPath: updateInfo.path, contents: Data()) {
|
|
720
|
-
|
|
777
|
+
logger.error("Cannot ensure that a file at \(updateInfo.path) exists")
|
|
721
778
|
}
|
|
722
779
|
}
|
|
723
780
|
}
|
|
@@ -729,7 +786,7 @@ import UIKit
|
|
|
729
786
|
do {
|
|
730
787
|
try fileManager.removeItem(at: tempDataPath)
|
|
731
788
|
} catch {
|
|
732
|
-
|
|
789
|
+
logger.error("Could not delete file at \(tempDataPath): \(error)")
|
|
733
790
|
}
|
|
734
791
|
}
|
|
735
792
|
// Deleting update.dat
|
|
@@ -737,7 +794,7 @@ import UIKit
|
|
|
737
794
|
do {
|
|
738
795
|
try fileManager.removeItem(at: updateInfo)
|
|
739
796
|
} catch {
|
|
740
|
-
|
|
797
|
+
logger.error("Could not delete file at \(updateInfo): \(error)")
|
|
741
798
|
}
|
|
742
799
|
}
|
|
743
800
|
}
|
|
@@ -756,7 +813,7 @@ import UIKit
|
|
|
756
813
|
fileHandle.closeFile()
|
|
757
814
|
}
|
|
758
815
|
} catch {
|
|
759
|
-
|
|
816
|
+
logger.error("Failed to write data starting at byte \(byteOffset): \(error)")
|
|
760
817
|
}
|
|
761
818
|
self.tempData.removeAll() // Clearing tempData to avoid writing the same data multiple times
|
|
762
819
|
}
|
|
@@ -765,7 +822,7 @@ import UIKit
|
|
|
765
822
|
do {
|
|
766
823
|
try "\(version)".write(to: updateInfo, atomically: true, encoding: .utf8)
|
|
767
824
|
} catch {
|
|
768
|
-
|
|
825
|
+
logger.error("Failed to save progress: \(error)")
|
|
769
826
|
}
|
|
770
827
|
}
|
|
771
828
|
private func getLocalUpdateVersion() -> String { // Return the version that was tried to be downloaded on last download attempt
|
|
@@ -787,7 +844,7 @@ import UIKit
|
|
|
787
844
|
return fileSize.int64Value
|
|
788
845
|
}
|
|
789
846
|
} catch {
|
|
790
|
-
|
|
847
|
+
logger.error("Could not retrieve already downloaded data size : \(error)")
|
|
791
848
|
}
|
|
792
849
|
return 0
|
|
793
850
|
}
|
|
@@ -799,7 +856,7 @@ import UIKit
|
|
|
799
856
|
do {
|
|
800
857
|
let files: [String] = try FileManager.default.contentsOfDirectory(atPath: dest.path)
|
|
801
858
|
var res: [BundleInfo] = []
|
|
802
|
-
|
|
859
|
+
logger.info("list File : \(dest.path)")
|
|
803
860
|
if dest.exist {
|
|
804
861
|
for id: String in files {
|
|
805
862
|
res.append(self.getBundleInfo(id: id))
|
|
@@ -807,12 +864,12 @@ import UIKit
|
|
|
807
864
|
}
|
|
808
865
|
return res
|
|
809
866
|
} catch {
|
|
810
|
-
|
|
867
|
+
logger.info("No version available \(dest.path)")
|
|
811
868
|
return []
|
|
812
869
|
}
|
|
813
870
|
} else {
|
|
814
871
|
guard let regex = try? NSRegularExpression(pattern: "^[0-9A-Za-z]{10}_info$") else {
|
|
815
|
-
|
|
872
|
+
logger.error("Invalid regex ?????")
|
|
816
873
|
return []
|
|
817
874
|
}
|
|
818
875
|
return UserDefaults.standard.dictionaryRepresentation().keys.filter {
|
|
@@ -831,7 +888,7 @@ import UIKit
|
|
|
831
888
|
public func delete(id: String, removeInfo: Bool) -> Bool {
|
|
832
889
|
let deleted: BundleInfo = self.getBundleInfo(id: id)
|
|
833
890
|
if deleted.isBuiltin() || self.getCurrentBundleId() == id {
|
|
834
|
-
|
|
891
|
+
logger.info("Cannot delete \(id)")
|
|
835
892
|
return false
|
|
836
893
|
}
|
|
837
894
|
|
|
@@ -840,7 +897,7 @@ import UIKit
|
|
|
840
897
|
!next.isDeleted() &&
|
|
841
898
|
!next.isErrorStatus() &&
|
|
842
899
|
next.getId() == id {
|
|
843
|
-
|
|
900
|
+
logger.info("Cannot delete the next bundle \(id)")
|
|
844
901
|
return false
|
|
845
902
|
}
|
|
846
903
|
|
|
@@ -848,7 +905,7 @@ import UIKit
|
|
|
848
905
|
do {
|
|
849
906
|
try FileManager.default.removeItem(atPath: destPersist.path)
|
|
850
907
|
} catch {
|
|
851
|
-
|
|
908
|
+
logger.error("Folder \(destPersist.path), not removed.")
|
|
852
909
|
// even if, we don;t care. Android doesn't care
|
|
853
910
|
if removeInfo {
|
|
854
911
|
self.removeBundleInfo(id: id)
|
|
@@ -861,7 +918,7 @@ import UIKit
|
|
|
861
918
|
} else {
|
|
862
919
|
self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.localizedString))
|
|
863
920
|
}
|
|
864
|
-
|
|
921
|
+
logger.info("bundle delete \(deleted.getVersionName())")
|
|
865
922
|
self.sendStats(action: "delete", versionName: deleted.getVersionName())
|
|
866
923
|
return true
|
|
867
924
|
}
|
|
@@ -870,6 +927,42 @@ import UIKit
|
|
|
870
927
|
return self.delete(id: id, removeInfo: true)
|
|
871
928
|
}
|
|
872
929
|
|
|
930
|
+
public func cleanupDownloadDirectories(allowedIds: Set<String>) {
|
|
931
|
+
let bundleRoot = libraryDir.appendingPathComponent(bundleDirectory)
|
|
932
|
+
let fileManager = FileManager.default
|
|
933
|
+
|
|
934
|
+
guard fileManager.fileExists(atPath: bundleRoot.path) else {
|
|
935
|
+
return
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
do {
|
|
939
|
+
let contents = try fileManager.contentsOfDirectory(at: bundleRoot, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
|
|
940
|
+
|
|
941
|
+
for url in contents {
|
|
942
|
+
let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey])
|
|
943
|
+
if resourceValues.isDirectory != true {
|
|
944
|
+
continue
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
let id = url.lastPathComponent
|
|
948
|
+
|
|
949
|
+
if allowedIds.contains(id) {
|
|
950
|
+
continue
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
do {
|
|
954
|
+
try fileManager.removeItem(at: url)
|
|
955
|
+
self.removeBundleInfo(id: id)
|
|
956
|
+
logger.info("Deleted orphan bundle directory: \(id)")
|
|
957
|
+
} catch {
|
|
958
|
+
logger.error("Failed to delete orphan bundle directory: \(id) \(error.localizedDescription)")
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
} catch {
|
|
962
|
+
logger.error("Failed to enumerate bundle directory for cleanup: \(error.localizedDescription)")
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
873
966
|
public func getBundleDirectory(id: String) -> URL {
|
|
874
967
|
return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
|
|
875
968
|
}
|
|
@@ -914,7 +1007,7 @@ import UIKit
|
|
|
914
1007
|
public func autoReset() {
|
|
915
1008
|
let currentBundle: BundleInfo = self.getCurrentBundle()
|
|
916
1009
|
if !currentBundle.isBuiltin() && !self.bundleExists(id: currentBundle.getId()) {
|
|
917
|
-
|
|
1010
|
+
logger.info("Folder at bundle path does not exist. Triggering reset.")
|
|
918
1011
|
self.reset()
|
|
919
1012
|
}
|
|
920
1013
|
}
|
|
@@ -924,7 +1017,7 @@ import UIKit
|
|
|
924
1017
|
}
|
|
925
1018
|
|
|
926
1019
|
public func reset(isInternal: Bool) {
|
|
927
|
-
|
|
1020
|
+
logger.info("reset: \(isInternal)")
|
|
928
1021
|
let currentBundleName = self.getCurrentBundle().getVersionName()
|
|
929
1022
|
self.setCurrentBundle(bundle: "")
|
|
930
1023
|
self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
|
|
@@ -937,14 +1030,14 @@ import UIKit
|
|
|
937
1030
|
public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
|
|
938
1031
|
self.setBundleStatus(id: bundle.getId(), status: BundleStatus.SUCCESS)
|
|
939
1032
|
let fallback: BundleInfo = self.getFallbackBundle()
|
|
940
|
-
|
|
941
|
-
|
|
1033
|
+
logger.info("Fallback bundle is: \(fallback.toString())")
|
|
1034
|
+
logger.info("Version successfully loaded: \(bundle.toString())")
|
|
942
1035
|
if autoDeletePrevious && !fallback.isBuiltin() && fallback.getId() != bundle.getId() {
|
|
943
1036
|
let res = self.delete(id: fallback.getId())
|
|
944
1037
|
if res {
|
|
945
|
-
|
|
1038
|
+
logger.info("Deleted previous bundle: \(fallback.toString())")
|
|
946
1039
|
} else {
|
|
947
|
-
|
|
1040
|
+
logger.error("Failed to delete previous bundle: \(fallback.toString())")
|
|
948
1041
|
}
|
|
949
1042
|
}
|
|
950
1043
|
self.setFallbackBundle(fallback: bundle)
|
|
@@ -956,8 +1049,17 @@ import UIKit
|
|
|
956
1049
|
|
|
957
1050
|
func unsetChannel() -> SetChannel {
|
|
958
1051
|
let setChannel: SetChannel = SetChannel()
|
|
1052
|
+
|
|
1053
|
+
// Check if rate limit was exceeded
|
|
1054
|
+
if CapgoUpdater.rateLimitExceeded {
|
|
1055
|
+
logger.debug("Skipping unsetChannel due to rate limit (429). Requests will resume after app restart.")
|
|
1056
|
+
setChannel.message = "Rate limit exceeded"
|
|
1057
|
+
setChannel.error = "rate_limit_exceeded"
|
|
1058
|
+
return setChannel
|
|
1059
|
+
}
|
|
1060
|
+
|
|
959
1061
|
if (self.channelUrl ).isEmpty {
|
|
960
|
-
|
|
1062
|
+
logger.error("Channel URL is not set")
|
|
961
1063
|
setChannel.message = "Channel URL is not set"
|
|
962
1064
|
setChannel.error = "missing_config"
|
|
963
1065
|
return setChannel
|
|
@@ -965,24 +1067,30 @@ import UIKit
|
|
|
965
1067
|
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
|
966
1068
|
let parameters: InfoObject = self.createInfoObject()
|
|
967
1069
|
|
|
968
|
-
let request =
|
|
1070
|
+
let request = alamofireSession.request(self.channelUrl, method: .delete, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
|
|
969
1071
|
|
|
970
1072
|
request.validate().responseDecodable(of: SetChannelDec.self) { response in
|
|
1073
|
+
// Check for 429 rate limit
|
|
1074
|
+
if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
|
|
1075
|
+
setChannel.message = "Rate limit exceeded"
|
|
1076
|
+
setChannel.error = "rate_limit_exceeded"
|
|
1077
|
+
semaphore.signal()
|
|
1078
|
+
return
|
|
1079
|
+
}
|
|
1080
|
+
|
|
971
1081
|
switch response.result {
|
|
972
1082
|
case .success:
|
|
973
|
-
if let
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
setChannel.message = message
|
|
1083
|
+
if let responseValue = response.value {
|
|
1084
|
+
if let error = responseValue.error {
|
|
1085
|
+
setChannel.error = error
|
|
1086
|
+
} else {
|
|
1087
|
+
setChannel.status = responseValue.status ?? ""
|
|
1088
|
+
setChannel.message = responseValue.message ?? ""
|
|
1089
|
+
}
|
|
981
1090
|
}
|
|
982
1091
|
case let .failure(error):
|
|
983
|
-
|
|
984
|
-
setChannel.
|
|
985
|
-
setChannel.error = "response_error"
|
|
1092
|
+
self.logger.error("Error unset Channel \(error)")
|
|
1093
|
+
setChannel.error = "Request failed: \(error.localizedDescription)"
|
|
986
1094
|
}
|
|
987
1095
|
semaphore.signal()
|
|
988
1096
|
}
|
|
@@ -992,8 +1100,17 @@ import UIKit
|
|
|
992
1100
|
|
|
993
1101
|
func setChannel(channel: String) -> SetChannel {
|
|
994
1102
|
let setChannel: SetChannel = SetChannel()
|
|
1103
|
+
|
|
1104
|
+
// Check if rate limit was exceeded
|
|
1105
|
+
if CapgoUpdater.rateLimitExceeded {
|
|
1106
|
+
logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.")
|
|
1107
|
+
setChannel.message = "Rate limit exceeded"
|
|
1108
|
+
setChannel.error = "rate_limit_exceeded"
|
|
1109
|
+
return setChannel
|
|
1110
|
+
}
|
|
1111
|
+
|
|
995
1112
|
if (self.channelUrl ).isEmpty {
|
|
996
|
-
|
|
1113
|
+
logger.error("Channel URL is not set")
|
|
997
1114
|
setChannel.message = "Channel URL is not set"
|
|
998
1115
|
setChannel.error = "missing_config"
|
|
999
1116
|
return setChannel
|
|
@@ -1002,24 +1119,30 @@ import UIKit
|
|
|
1002
1119
|
var parameters: InfoObject = self.createInfoObject()
|
|
1003
1120
|
parameters.channel = channel
|
|
1004
1121
|
|
|
1005
|
-
let request =
|
|
1122
|
+
let request = alamofireSession.request(self.channelUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
|
|
1006
1123
|
|
|
1007
1124
|
request.validate().responseDecodable(of: SetChannelDec.self) { response in
|
|
1125
|
+
// Check for 429 rate limit
|
|
1126
|
+
if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
|
|
1127
|
+
setChannel.message = "Rate limit exceeded"
|
|
1128
|
+
setChannel.error = "rate_limit_exceeded"
|
|
1129
|
+
semaphore.signal()
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1008
1133
|
switch response.result {
|
|
1009
1134
|
case .success:
|
|
1010
|
-
if let
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
setChannel.message = message
|
|
1135
|
+
if let responseValue = response.value {
|
|
1136
|
+
if let error = responseValue.error {
|
|
1137
|
+
setChannel.error = error
|
|
1138
|
+
} else {
|
|
1139
|
+
setChannel.status = responseValue.status ?? ""
|
|
1140
|
+
setChannel.message = responseValue.message ?? ""
|
|
1141
|
+
}
|
|
1018
1142
|
}
|
|
1019
1143
|
case let .failure(error):
|
|
1020
|
-
|
|
1021
|
-
setChannel.
|
|
1022
|
-
setChannel.error = "response_error"
|
|
1144
|
+
self.logger.error("Error set Channel \(error)")
|
|
1145
|
+
setChannel.error = "Request failed: \(error.localizedDescription)"
|
|
1023
1146
|
}
|
|
1024
1147
|
semaphore.signal()
|
|
1025
1148
|
}
|
|
@@ -1029,36 +1152,48 @@ import UIKit
|
|
|
1029
1152
|
|
|
1030
1153
|
func getChannel() -> GetChannel {
|
|
1031
1154
|
let getChannel: GetChannel = GetChannel()
|
|
1155
|
+
|
|
1156
|
+
// Check if rate limit was exceeded
|
|
1157
|
+
if CapgoUpdater.rateLimitExceeded {
|
|
1158
|
+
logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.")
|
|
1159
|
+
getChannel.message = "Rate limit exceeded"
|
|
1160
|
+
getChannel.error = "rate_limit_exceeded"
|
|
1161
|
+
return getChannel
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1032
1164
|
if (self.channelUrl ).isEmpty {
|
|
1033
|
-
|
|
1165
|
+
logger.error("Channel URL is not set")
|
|
1034
1166
|
getChannel.message = "Channel URL is not set"
|
|
1035
1167
|
getChannel.error = "missing_config"
|
|
1036
1168
|
return getChannel
|
|
1037
1169
|
}
|
|
1038
1170
|
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
|
1039
1171
|
let parameters: InfoObject = self.createInfoObject()
|
|
1040
|
-
let request =
|
|
1172
|
+
let request = alamofireSession.request(self.channelUrl, method: .put, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
|
|
1041
1173
|
|
|
1042
1174
|
request.validate().responseDecodable(of: GetChannelDec.self) { response in
|
|
1043
1175
|
defer {
|
|
1044
1176
|
semaphore.signal()
|
|
1045
1177
|
}
|
|
1178
|
+
|
|
1179
|
+
// Check for 429 rate limit
|
|
1180
|
+
if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
|
|
1181
|
+
getChannel.message = "Rate limit exceeded"
|
|
1182
|
+
getChannel.error = "rate_limit_exceeded"
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1046
1186
|
switch response.result {
|
|
1047
1187
|
case .success:
|
|
1048
|
-
if let
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
if let channel = response.value?.channel {
|
|
1058
|
-
getChannel.channel = channel
|
|
1059
|
-
}
|
|
1060
|
-
if let allowSet = response.value?.allowSet {
|
|
1061
|
-
getChannel.allowSet = allowSet
|
|
1188
|
+
if let responseValue = response.value {
|
|
1189
|
+
if let error = responseValue.error {
|
|
1190
|
+
getChannel.error = error
|
|
1191
|
+
} else {
|
|
1192
|
+
getChannel.status = responseValue.status ?? ""
|
|
1193
|
+
getChannel.message = responseValue.message ?? ""
|
|
1194
|
+
getChannel.channel = responseValue.channel ?? ""
|
|
1195
|
+
getChannel.allowSet = responseValue.allowSet ?? true
|
|
1196
|
+
}
|
|
1062
1197
|
}
|
|
1063
1198
|
case let .failure(error):
|
|
1064
1199
|
if let data = response.data, let bodyString = String(data: data, encoding: .utf8) {
|
|
@@ -1069,18 +1204,105 @@ import UIKit
|
|
|
1069
1204
|
}
|
|
1070
1205
|
}
|
|
1071
1206
|
|
|
1072
|
-
|
|
1073
|
-
getChannel.
|
|
1074
|
-
getChannel.error = "response_error"
|
|
1207
|
+
self.logger.error("Error get Channel \(error)")
|
|
1208
|
+
getChannel.error = "Request failed: \(error.localizedDescription)"
|
|
1075
1209
|
}
|
|
1076
1210
|
}
|
|
1077
1211
|
semaphore.wait()
|
|
1078
1212
|
return getChannel
|
|
1079
1213
|
}
|
|
1080
1214
|
|
|
1215
|
+
func listChannels() -> ListChannels {
|
|
1216
|
+
let listChannels: ListChannels = ListChannels()
|
|
1217
|
+
|
|
1218
|
+
// Check if rate limit was exceeded
|
|
1219
|
+
if CapgoUpdater.rateLimitExceeded {
|
|
1220
|
+
logger.debug("Skipping listChannels due to rate limit (429). Requests will resume after app restart.")
|
|
1221
|
+
listChannels.error = "rate_limit_exceeded"
|
|
1222
|
+
return listChannels
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (self.channelUrl).isEmpty {
|
|
1226
|
+
logger.error("Channel URL is not set")
|
|
1227
|
+
listChannels.error = "Channel URL is not set"
|
|
1228
|
+
return listChannels
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
|
1232
|
+
|
|
1233
|
+
// Auto-detect values
|
|
1234
|
+
let appId = self.appId
|
|
1235
|
+
let platform = "ios"
|
|
1236
|
+
let isEmulator = self.isEmulator()
|
|
1237
|
+
let isProd = self.isProd()
|
|
1238
|
+
|
|
1239
|
+
// Create query parameters
|
|
1240
|
+
var urlComponents = URLComponents(string: self.channelUrl)
|
|
1241
|
+
urlComponents?.queryItems = [
|
|
1242
|
+
URLQueryItem(name: "app_id", value: appId),
|
|
1243
|
+
URLQueryItem(name: "platform", value: platform),
|
|
1244
|
+
URLQueryItem(name: "is_emulator", value: String(isEmulator)),
|
|
1245
|
+
URLQueryItem(name: "is_prod", value: String(isProd))
|
|
1246
|
+
]
|
|
1247
|
+
|
|
1248
|
+
guard let url = urlComponents?.url else {
|
|
1249
|
+
logger.error("Invalid channel URL")
|
|
1250
|
+
listChannels.error = "Invalid channel URL"
|
|
1251
|
+
return listChannels
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
let request = alamofireSession.request(url, method: .get, requestModifier: { $0.timeoutInterval = self.timeout })
|
|
1255
|
+
|
|
1256
|
+
request.validate().responseDecodable(of: ListChannelsDec.self) { response in
|
|
1257
|
+
defer {
|
|
1258
|
+
semaphore.signal()
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Check for 429 rate limit
|
|
1262
|
+
if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
|
|
1263
|
+
listChannels.error = "rate_limit_exceeded"
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
switch response.result {
|
|
1268
|
+
case .success:
|
|
1269
|
+
if let responseValue = response.value {
|
|
1270
|
+
// Check for server-side errors
|
|
1271
|
+
if let error = responseValue.error {
|
|
1272
|
+
listChannels.error = error
|
|
1273
|
+
return
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Backend returns direct array, so channels should be populated by our custom decoder
|
|
1277
|
+
if let channels = responseValue.channels {
|
|
1278
|
+
listChannels.channels = channels.map { channel in
|
|
1279
|
+
var channelDict: [String: Any] = [:]
|
|
1280
|
+
channelDict["id"] = channel.id ?? ""
|
|
1281
|
+
channelDict["name"] = channel.name ?? ""
|
|
1282
|
+
channelDict["public"] = channel.public ?? false
|
|
1283
|
+
channelDict["allow_self_set"] = channel.allow_self_set ?? false
|
|
1284
|
+
return channelDict
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
case let .failure(error):
|
|
1289
|
+
self.logger.error("Error list channels \(error)")
|
|
1290
|
+
listChannels.error = "Request failed: \(error.localizedDescription)"
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
semaphore.wait()
|
|
1294
|
+
return listChannels
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1081
1297
|
private let operationQueue = OperationQueue()
|
|
1082
1298
|
|
|
1083
1299
|
func sendStats(action: String, versionName: String? = nil, oldVersionName: String? = "") {
|
|
1300
|
+
// Check if rate limit was exceeded
|
|
1301
|
+
if CapgoUpdater.rateLimitExceeded {
|
|
1302
|
+
logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.")
|
|
1303
|
+
return
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1084
1306
|
guard !statsUrl.isEmpty else {
|
|
1085
1307
|
return
|
|
1086
1308
|
}
|
|
@@ -1095,22 +1317,28 @@ import UIKit
|
|
|
1095
1317
|
|
|
1096
1318
|
let operation = BlockOperation {
|
|
1097
1319
|
let semaphore = DispatchSemaphore(value: 0)
|
|
1098
|
-
|
|
1320
|
+
self.alamofireSession.request(
|
|
1099
1321
|
self.statsUrl,
|
|
1100
1322
|
method: .post,
|
|
1101
1323
|
parameters: parameters,
|
|
1102
1324
|
encoder: JSONParameterEncoder.default,
|
|
1103
1325
|
requestModifier: { $0.timeoutInterval = self.timeout }
|
|
1104
1326
|
).responseData { response in
|
|
1327
|
+
// Check for 429 rate limit
|
|
1328
|
+
if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
|
|
1329
|
+
semaphore.signal()
|
|
1330
|
+
return
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1105
1333
|
switch response.result {
|
|
1106
1334
|
case .success:
|
|
1107
|
-
|
|
1335
|
+
self.logger.info("Stats sent for \(action), version \(versionName)")
|
|
1108
1336
|
case let .failure(error):
|
|
1109
|
-
|
|
1337
|
+
self.logger.error("Error sending stats: \(response.value?.debugDescription ?? "") \(error.localizedDescription)")
|
|
1110
1338
|
}
|
|
1111
1339
|
semaphore.signal()
|
|
1112
1340
|
}
|
|
1113
|
-
semaphore.
|
|
1341
|
+
semaphore.wait()
|
|
1114
1342
|
}
|
|
1115
1343
|
operationQueue.addOperation(operation)
|
|
1116
1344
|
|
|
@@ -1121,7 +1349,6 @@ import UIKit
|
|
|
1121
1349
|
if id != nil {
|
|
1122
1350
|
trueId = id!
|
|
1123
1351
|
}
|
|
1124
|
-
// print("\(CapacitorUpdater.TAG) Getting info for bundle [\(trueId)]")
|
|
1125
1352
|
let result: BundleInfo
|
|
1126
1353
|
if BundleInfo.ID_BUILTIN == trueId {
|
|
1127
1354
|
result = BundleInfo(id: trueId, version: "", status: BundleStatus.SUCCESS, checksum: "")
|
|
@@ -1131,11 +1358,10 @@ import UIKit
|
|
|
1131
1358
|
do {
|
|
1132
1359
|
result = try UserDefaults.standard.getObj(forKey: "\(trueId)\(self.INFO_SUFFIX)", castTo: BundleInfo.self)
|
|
1133
1360
|
} catch {
|
|
1134
|
-
|
|
1361
|
+
logger.error("Failed to parse info for bundle [\(trueId)] \(error.localizedDescription)")
|
|
1135
1362
|
result = BundleInfo(id: trueId, version: "", status: BundleStatus.PENDING, checksum: "")
|
|
1136
1363
|
}
|
|
1137
1364
|
}
|
|
1138
|
-
// print("\(CapacitorUpdater.TAG) Returning info bundle [\(result.toString())]")
|
|
1139
1365
|
return result
|
|
1140
1366
|
}
|
|
1141
1367
|
|
|
@@ -1155,26 +1381,26 @@ import UIKit
|
|
|
1155
1381
|
|
|
1156
1382
|
public func saveBundleInfo(id: String, bundle: BundleInfo?) {
|
|
1157
1383
|
if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
|
|
1158
|
-
|
|
1384
|
+
logger.info("Not saving info for bundle [\(id)] \(bundle?.toString() ?? "")")
|
|
1159
1385
|
return
|
|
1160
1386
|
}
|
|
1161
1387
|
if bundle == nil {
|
|
1162
|
-
|
|
1388
|
+
logger.info("Removing info for bundle [\(id)]")
|
|
1163
1389
|
UserDefaults.standard.removeObject(forKey: "\(id)\(self.INFO_SUFFIX)")
|
|
1164
1390
|
} else {
|
|
1165
1391
|
let update = bundle!.setId(id: id)
|
|
1166
|
-
|
|
1392
|
+
logger.info("Storing info for bundle [\(id)] \(update.toString())")
|
|
1167
1393
|
do {
|
|
1168
1394
|
try UserDefaults.standard.setObj(update, forKey: "\(id)\(self.INFO_SUFFIX)")
|
|
1169
1395
|
} catch {
|
|
1170
|
-
|
|
1396
|
+
logger.error("Failed to save info for bundle [\(id)] \(error.localizedDescription)")
|
|
1171
1397
|
}
|
|
1172
1398
|
}
|
|
1173
1399
|
UserDefaults.standard.synchronize()
|
|
1174
1400
|
}
|
|
1175
1401
|
|
|
1176
1402
|
private func setBundleStatus(id: String, status: BundleStatus) {
|
|
1177
|
-
|
|
1403
|
+
logger.info("Setting status for bundle [\(id)] to \(status)")
|
|
1178
1404
|
let info = self.getBundleInfo(id: id)
|
|
1179
1405
|
self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.localizedString))
|
|
1180
1406
|
}
|