@capawesome/capacitor-live-update 6.0.8 → 6.2.0-dev.0a2a6a5.1722676620
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 +34 -14
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +136 -19
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +26 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +31 -9
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +17 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +29 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +11 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBundleResult.java +2 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
- package/dist/docs.json +117 -9
- package/dist/esm/definitions.d.ts +62 -8
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -2
- package/dist/esm/web.js.map +1 -1
- package/ios/Plugin/Classes/CustomError.swift +12 -0
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -1
- package/ios/Plugin/Classes/Results/GetBundleResult.swift +1 -1
- package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
- package/ios/Plugin/LiveUpdate.swift +145 -15
- package/ios/Plugin/LiveUpdateConfig.swift +2 -0
- package/ios/Plugin/LiveUpdatePlugin.swift +15 -4
- package/ios/Plugin/LiveUpdatePreferences.swift +14 -0
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,11 +7,13 @@ Capacitor plugin to update your app remotely in real-time.
|
|
|
7
7
|
- 🔋 Supports **Android and iOS**
|
|
8
8
|
- ⚡️ **Capacitor 6** support
|
|
9
9
|
- 📦 **Bundle Management**: Download, set, and delete bundles.
|
|
10
|
-
- ☁️ **Cloud Support**: Use the [Capawesome Cloud](https://capawesome.io/cloud) to manage your app updates.
|
|
10
|
+
- ☁️ **Cloud Support**: Use the [Capawesome Cloud](https://capawesome.io/cloud/live-updates/) to manage your app updates.
|
|
11
11
|
- 📺 **Channel Support**: Set a channel for the app to manage different versions.
|
|
12
12
|
- 🔄 **Auto Update**: Automatically download and set the latest bundle for the app.
|
|
13
13
|
- 🛟 **Rollback**: Reset the app to the default bundle if an incompatible bundle has been set.
|
|
14
14
|
- 🚀 **Rollout**: Gradually roll out new bundles to gather valuable feedback.
|
|
15
|
+
- 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
|
|
16
|
+
- 🌐 **Open Source**: Licensed under the MIT License.
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -63,13 +65,15 @@ We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bun
|
|
|
63
65
|
<docgen-config>
|
|
64
66
|
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
65
67
|
|
|
66
|
-
| Prop | Type
|
|
67
|
-
| ----------------------- |
|
|
68
|
-
| **`appId`** | <code>string</code>
|
|
69
|
-
| **`autoDeleteBundles`** | <code>boolean</code>
|
|
70
|
-
| **`enabled`** | <code>boolean</code>
|
|
71
|
-
| **`
|
|
72
|
-
| **`
|
|
68
|
+
| Prop | Type | Description | Default | Since |
|
|
69
|
+
| ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
|
|
70
|
+
| **`appId`** | <code>string</code> | The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud). | | 5.0.0 |
|
|
71
|
+
| **`autoDeleteBundles`** | <code>boolean</code> | Whether or not to automatically delete unused bundles. When enabled, the plugin will automatically delete unused bundles after calling `ready()`. | <code>false</code> | 5.0.0 |
|
|
72
|
+
| **`enabled`** | <code>boolean</code> | Whether or not the plugin is enabled. | <code>true</code> | 5.0.0 |
|
|
73
|
+
| **`location`** | <code>'us' \| 'eu'</code> | The location of the server to use when using [Capawesome Cloud](https://capawesome.io/cloud). | <code>'us'</code> | 6.2.0 |
|
|
74
|
+
| **`publicKey`** | <code>string</code> | The public key to verify the integrity of the bundle. The public key must be a PEM-encoded RSA public key. | | 6.1.0 |
|
|
75
|
+
| **`readyTimeout`** | <code>number</code> | The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle. | <code>10000</code> | 5.0.0 |
|
|
76
|
+
| **`resetOnUpdate`** | <code>boolean</code> | Whether or not the app should be reset to the default bundle during an update. | <code>true</code> | 5.0.0 |
|
|
73
77
|
|
|
74
78
|
### Examples
|
|
75
79
|
|
|
@@ -82,6 +86,8 @@ In `capacitor.config.json`:
|
|
|
82
86
|
"appId": '4100e356-e851-47fe-9d3c-b411eb325a99',
|
|
83
87
|
"autoDeleteBundles": undefined,
|
|
84
88
|
"enabled": undefined,
|
|
89
|
+
"location": 'eu',
|
|
90
|
+
"publicKey": '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----',
|
|
85
91
|
"readyTimeout": undefined,
|
|
86
92
|
"resetOnUpdate": undefined
|
|
87
93
|
}
|
|
@@ -102,6 +108,8 @@ const config: CapacitorConfig = {
|
|
|
102
108
|
appId: '4100e356-e851-47fe-9d3c-b411eb325a99',
|
|
103
109
|
autoDeleteBundles: undefined,
|
|
104
110
|
enabled: undefined,
|
|
111
|
+
location: 'eu',
|
|
112
|
+
publicKey: '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----',
|
|
105
113
|
readyTimeout: undefined,
|
|
106
114
|
resetOnUpdate: undefined,
|
|
107
115
|
},
|
|
@@ -393,7 +401,7 @@ Only available on Android and iOS.
|
|
|
393
401
|
### ready()
|
|
394
402
|
|
|
395
403
|
```typescript
|
|
396
|
-
ready() => Promise<
|
|
404
|
+
ready() => Promise<ReadyResult>
|
|
397
405
|
```
|
|
398
406
|
|
|
399
407
|
Notify the plugin that the app is ready to use and no rollback is needed.
|
|
@@ -403,6 +411,8 @@ to prevent the app from being reset to the default bundle.
|
|
|
403
411
|
|
|
404
412
|
Only available on Android and iOS.
|
|
405
413
|
|
|
414
|
+
**Returns:** <code>Promise<<a href="#readyresult">ReadyResult</a>></code>
|
|
415
|
+
|
|
406
416
|
**Since:** 5.0.0
|
|
407
417
|
|
|
408
418
|
--------------------
|
|
@@ -530,10 +540,11 @@ Only available on Android and iOS.
|
|
|
530
540
|
|
|
531
541
|
#### DownloadBundleOptions
|
|
532
542
|
|
|
533
|
-
| Prop | Type | Description
|
|
534
|
-
| -------------- | ------------------- |
|
|
535
|
-
| **`
|
|
536
|
-
| **`
|
|
543
|
+
| Prop | Type | Description | Since |
|
|
544
|
+
| -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------- | ----- |
|
|
545
|
+
| **`bundleId`** | <code>string</code> | The unique identifier of the bundle. | 5.0.0 |
|
|
546
|
+
| **`checksum`** | <code>string</code> | The checksum of the bundle to verify the integrity of the ZIP file. Must be a SHA-256 hash in hexadecimal format. | 6.1.0 |
|
|
547
|
+
| **`url`** | <code>string</code> | The URL of the bundle to download. The bundle must be a ZIP file containing at least a `index.html` file. | 5.0.0 |
|
|
537
548
|
|
|
538
549
|
|
|
539
550
|
#### GetBundleResult
|
|
@@ -585,6 +596,15 @@ Only available on Android and iOS.
|
|
|
585
596
|
| **`versionName`** | <code>string</code> | The version name of the app. On **Android**, this is the `versionName` from the `android/app/build.gradle` file. On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file. | 5.0.0 |
|
|
586
597
|
|
|
587
598
|
|
|
599
|
+
#### ReadyResult
|
|
600
|
+
|
|
601
|
+
| Prop | Type | Description | Since |
|
|
602
|
+
| ---------------------- | --------------------------- | --------------------------------------------------------------------------------------- | ----- |
|
|
603
|
+
| **`previousBundleId`** | <code>string \| null</code> | The identifier of the previous bundle used. If `null`, the default bundle was used. | 7.0.0 |
|
|
604
|
+
| **`currentBundleId`** | <code>string \| null</code> | The identifier of the current bundle used. If `null`, the default bundle is being used. | 7.0.0 |
|
|
605
|
+
| **`rollback`** | <code>boolean</code> | Whether or not the app was reset to the default bundle. | 7.0.0 |
|
|
606
|
+
|
|
607
|
+
|
|
588
608
|
#### SetBundleOptions
|
|
589
609
|
|
|
590
610
|
| Prop | Type | Description | Since |
|
|
@@ -630,7 +650,7 @@ If you want to disable the plugin to test other parts of your app, you can set t
|
|
|
630
650
|
|
|
631
651
|
Live updates are only supported for [binary-compatible changes](https://capawesome.io/cloud/live-updates/faq/#what-are-binary-compatible-changes) (e.g. HTML, CSS, JavaScript).
|
|
632
652
|
If you change native code, such as adding a new plugin, you need to resubmit your app to the app stores.
|
|
633
|
-
For this reason, you must be careful to
|
|
653
|
+
For this reason, you must be careful to [restrict live updates to compatible native versions](https://capawesome.io/blog/how-to-restrict-capacitor-live-updates-to-native-versions/) of your app.
|
|
634
654
|
|
|
635
655
|
## Changelog
|
|
636
656
|
|
|
@@ -6,8 +6,10 @@ import android.content.pm.PackageInfo;
|
|
|
6
6
|
import android.content.pm.PackageManager;
|
|
7
7
|
import android.os.Build;
|
|
8
8
|
import android.os.Handler;
|
|
9
|
+
import android.util.Base64;
|
|
9
10
|
import androidx.annotation.NonNull;
|
|
10
11
|
import androidx.annotation.Nullable;
|
|
12
|
+
import com.getcapacitor.Bridge;
|
|
11
13
|
import com.getcapacitor.Logger;
|
|
12
14
|
import com.getcapacitor.plugin.WebView;
|
|
13
15
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
|
|
@@ -23,12 +25,18 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCustomIdR
|
|
|
23
25
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdResult;
|
|
24
26
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
|
|
25
27
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
|
|
28
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ReadyResult;
|
|
26
29
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
|
|
27
30
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
28
31
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
29
32
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
30
33
|
import java.io.File;
|
|
31
34
|
import java.io.IOException;
|
|
35
|
+
import java.security.KeyFactory;
|
|
36
|
+
import java.security.MessageDigest;
|
|
37
|
+
import java.security.PublicKey;
|
|
38
|
+
import java.security.Signature;
|
|
39
|
+
import java.security.spec.X509EncodedKeySpec;
|
|
32
40
|
import java.util.Arrays;
|
|
33
41
|
import java.util.UUID;
|
|
34
42
|
import net.lingala.zip4j.ZipFile;
|
|
@@ -48,11 +56,11 @@ import org.json.JSONObject;
|
|
|
48
56
|
|
|
49
57
|
public class LiveUpdate {
|
|
50
58
|
|
|
59
|
+
public static final String DEFAULT_WEB_ASSET_DIR = Bridge.DEFAULT_WEB_ASSET_DIR;
|
|
60
|
+
|
|
51
61
|
@NonNull
|
|
52
62
|
private final LiveUpdateConfig config;
|
|
53
63
|
|
|
54
|
-
private final String defaultWebAssetDir;
|
|
55
|
-
|
|
56
64
|
@NonNull
|
|
57
65
|
private final LiveUpdatePlugin plugin;
|
|
58
66
|
|
|
@@ -65,9 +73,10 @@ public class LiveUpdate {
|
|
|
65
73
|
private final String bundlesDirectory = "_capacitor_live_update_bundles";
|
|
66
74
|
private final Handler rollbackHandler = new Handler();
|
|
67
75
|
|
|
76
|
+
private boolean rollbackPerformed = false;
|
|
77
|
+
|
|
68
78
|
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
|
|
69
79
|
this.config = config;
|
|
70
|
-
this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
|
|
71
80
|
this.plugin = plugin;
|
|
72
81
|
this.preferences = new LiveUpdatePreferences(plugin.getContext());
|
|
73
82
|
this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
@@ -97,6 +106,7 @@ public class LiveUpdate {
|
|
|
97
106
|
|
|
98
107
|
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
|
|
99
108
|
String bundleId = options.getBundleId();
|
|
109
|
+
String checksum = options.getChecksum();
|
|
100
110
|
String url = options.getUrl();
|
|
101
111
|
|
|
102
112
|
// Check if the bundle already exists
|
|
@@ -107,14 +117,11 @@ public class LiveUpdate {
|
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
// Download the bundle
|
|
110
|
-
downloadBundle(bundleId, url, callback);
|
|
120
|
+
downloadBundle(bundleId, checksum, null, url, callback);
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
public void getBundle(@NonNull NonEmptyCallback callback) {
|
|
114
124
|
String bundleId = getCurrentBundleId();
|
|
115
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
116
|
-
bundleId = null;
|
|
117
|
-
}
|
|
118
125
|
GetBundleResult result = new GetBundleResult(bundleId);
|
|
119
126
|
callback.success(result);
|
|
120
127
|
}
|
|
@@ -155,12 +162,18 @@ public class LiveUpdate {
|
|
|
155
162
|
callback.success(result);
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
public void ready() {
|
|
165
|
+
public void ready(@NonNull NonEmptyCallback callback) {
|
|
159
166
|
Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
|
|
160
167
|
stopRollbackTimer();
|
|
161
168
|
if (config.getAutoDeleteBundles()) {
|
|
162
169
|
deleteUnusedBundles();
|
|
163
170
|
}
|
|
171
|
+
String currentBundleId = getCurrentBundleId();
|
|
172
|
+
String previousBundleId = getPreviousBundleId();
|
|
173
|
+
ReadyResult result = new ReadyResult(currentBundleId, previousBundleId, rollbackPerformed);
|
|
174
|
+
callback.success(result);
|
|
175
|
+
setPreviousBundleId(currentBundleId);
|
|
176
|
+
rollbackPerformed = false;
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
public void reload() {
|
|
@@ -215,6 +228,8 @@ public class LiveUpdate {
|
|
|
215
228
|
@Override
|
|
216
229
|
public void success(@NonNull GetLatestBundleResponse result) {
|
|
217
230
|
String latestBundleId = result.getBundleId();
|
|
231
|
+
String checksum = result.getChecksum();
|
|
232
|
+
String signature = result.getSignature();
|
|
218
233
|
String url = result.getUrl();
|
|
219
234
|
|
|
220
235
|
// Check if the bundle already exists
|
|
@@ -233,6 +248,8 @@ public class LiveUpdate {
|
|
|
233
248
|
// Download and unzip the bundle
|
|
234
249
|
downloadBundle(
|
|
235
250
|
latestBundleId,
|
|
251
|
+
checksum,
|
|
252
|
+
signature,
|
|
236
253
|
url,
|
|
237
254
|
new EmptyCallback() {
|
|
238
255
|
@Override
|
|
@@ -290,6 +307,31 @@ public class LiveUpdate {
|
|
|
290
307
|
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundle);
|
|
291
308
|
}
|
|
292
309
|
|
|
310
|
+
private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
|
|
311
|
+
try {
|
|
312
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
313
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
314
|
+
Buffer buffer = new Buffer();
|
|
315
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
316
|
+
digest.update(buffer.readByteArray());
|
|
317
|
+
}
|
|
318
|
+
source.close();
|
|
319
|
+
return digest.digest();
|
|
320
|
+
} catch (IOException exception) {
|
|
321
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
322
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private String getChecksumForFileAsString(@NonNull File file) throws Exception {
|
|
327
|
+
byte[] checksumBytes = getChecksumForFileAsBytes(file);
|
|
328
|
+
StringBuilder checksum = new StringBuilder();
|
|
329
|
+
for (byte checksumByte : checksumBytes) {
|
|
330
|
+
checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
|
|
331
|
+
}
|
|
332
|
+
return checksum.toString();
|
|
333
|
+
}
|
|
334
|
+
|
|
293
335
|
private void createBundlesDirectory() {
|
|
294
336
|
File bundlesDirectory = buildBundlesDirectory();
|
|
295
337
|
if (!bundlesDirectory.exists()) {
|
|
@@ -297,6 +339,19 @@ public class LiveUpdate {
|
|
|
297
339
|
}
|
|
298
340
|
}
|
|
299
341
|
|
|
342
|
+
private PublicKey createPublicKeyFromString(@NonNull String value) throws Exception {
|
|
343
|
+
try {
|
|
344
|
+
value = value.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\n", "");
|
|
345
|
+
byte[] byteKey = Base64.decode(value, Base64.DEFAULT);
|
|
346
|
+
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey);
|
|
347
|
+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
348
|
+
return keyFactory.generatePublic(X509publicKey);
|
|
349
|
+
} catch (Exception exception) {
|
|
350
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
351
|
+
throw new Exception(LiveUpdatePlugin.ERROR_PUBLIC_KEY_INVALID);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
300
355
|
private void deleteBundle(@NonNull String bundleId) {
|
|
301
356
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
302
357
|
deleteFileRecursively(bundleDirectory);
|
|
@@ -320,7 +375,13 @@ public class LiveUpdate {
|
|
|
320
375
|
}
|
|
321
376
|
}
|
|
322
377
|
|
|
323
|
-
private void downloadBundle(
|
|
378
|
+
private void downloadBundle(
|
|
379
|
+
@NonNull String bundleId,
|
|
380
|
+
@Nullable String expectedChecksum,
|
|
381
|
+
@Nullable String signature,
|
|
382
|
+
@NonNull String url,
|
|
383
|
+
@NonNull EmptyCallback callback
|
|
384
|
+
) {
|
|
324
385
|
downloadFile(
|
|
325
386
|
url,
|
|
326
387
|
new NonEmptyCallback<File>() {
|
|
@@ -332,6 +393,28 @@ public class LiveUpdate {
|
|
|
332
393
|
@Override
|
|
333
394
|
public void success(@NonNull File result) {
|
|
334
395
|
try {
|
|
396
|
+
// Verify the signature
|
|
397
|
+
String publicKey = config.getPublicKey();
|
|
398
|
+
if (publicKey != null) {
|
|
399
|
+
if (signature == null) {
|
|
400
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
|
|
401
|
+
}
|
|
402
|
+
PublicKey key = createPublicKeyFromString(publicKey);
|
|
403
|
+
// Verify the signature
|
|
404
|
+
boolean verified = verifySignatureForFile(result, signature, key);
|
|
405
|
+
if (!verified) {
|
|
406
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Verify the checksum
|
|
410
|
+
else if (expectedChecksum != null) {
|
|
411
|
+
// Calculate the checksum
|
|
412
|
+
String receivedChecksum = getChecksumForFileAsString(result);
|
|
413
|
+
if (!expectedChecksum.equals(receivedChecksum)) {
|
|
414
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
335
418
|
// Add the bundle
|
|
336
419
|
addBundle(bundleId, result);
|
|
337
420
|
|
|
@@ -395,9 +478,13 @@ public class LiveUpdate {
|
|
|
395
478
|
@Nullable
|
|
396
479
|
private void fetchLatestBundle(@NonNull NonEmptyCallback<GetLatestBundleResponse> callback)
|
|
397
480
|
throws PackageManager.NameNotFoundException {
|
|
481
|
+
String host = "api.cloud.capawesome.io";
|
|
482
|
+
if (config.getLocation() != null && config.getLocation().equals("eu")) {
|
|
483
|
+
host = "api.cloud.capawesome.eu";
|
|
484
|
+
}
|
|
398
485
|
HttpUrl url = new HttpUrl.Builder()
|
|
399
486
|
.scheme("https")
|
|
400
|
-
.host(
|
|
487
|
+
.host(host)
|
|
401
488
|
.addPathSegment("v1")
|
|
402
489
|
.addPathSegment("apps")
|
|
403
490
|
.addPathSegment(config.getAppId())
|
|
@@ -477,8 +564,8 @@ public class LiveUpdate {
|
|
|
477
564
|
*/
|
|
478
565
|
private String getCurrentBundleId() {
|
|
479
566
|
String currentPath = getCurrentCapacitorServerPath();
|
|
480
|
-
if (currentPath.equals(
|
|
481
|
-
return
|
|
567
|
+
if (currentPath.equals(DEFAULT_WEB_ASSET_DIR)) {
|
|
568
|
+
return DEFAULT_WEB_ASSET_DIR;
|
|
482
569
|
}
|
|
483
570
|
return new File(currentPath).getName();
|
|
484
571
|
}
|
|
@@ -506,8 +593,8 @@ public class LiveUpdate {
|
|
|
506
593
|
@NonNull
|
|
507
594
|
private String getNextBundleId() {
|
|
508
595
|
String nextPath = getNextCapacitorServerPath();
|
|
509
|
-
if (nextPath.equals(
|
|
510
|
-
return
|
|
596
|
+
if (nextPath.equals(DEFAULT_WEB_ASSET_DIR)) {
|
|
597
|
+
return DEFAULT_WEB_ASSET_DIR;
|
|
511
598
|
}
|
|
512
599
|
return new File(nextPath).getName();
|
|
513
600
|
}
|
|
@@ -520,7 +607,12 @@ public class LiveUpdate {
|
|
|
520
607
|
return plugin
|
|
521
608
|
.getContext()
|
|
522
609
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
523
|
-
.getString(WebView.CAP_SERVER_PATH,
|
|
610
|
+
.getString(WebView.CAP_SERVER_PATH, DEFAULT_WEB_ASSET_DIR);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@Nullable
|
|
614
|
+
private String getPreviousBundleId() {
|
|
615
|
+
return preferences.getPreviousBundleId();
|
|
524
616
|
}
|
|
525
617
|
|
|
526
618
|
private boolean hasBundle(@NonNull String bundleId) {
|
|
@@ -535,7 +627,10 @@ public class LiveUpdate {
|
|
|
535
627
|
}
|
|
536
628
|
|
|
537
629
|
private void rollback() {
|
|
538
|
-
|
|
630
|
+
rollbackPerformed = true;
|
|
631
|
+
String currentBundleId = getCurrentBundleId();
|
|
632
|
+
setPreviousBundleId(currentBundleId);
|
|
633
|
+
if (currentBundleId.equals(DEFAULT_WEB_ASSET_DIR)) {
|
|
539
634
|
Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
|
|
540
635
|
return;
|
|
541
636
|
}
|
|
@@ -575,7 +670,7 @@ public class LiveUpdate {
|
|
|
575
670
|
}
|
|
576
671
|
|
|
577
672
|
private void setCurrentCapacitorServerPath(@NonNull String path) {
|
|
578
|
-
if (path.equals(
|
|
673
|
+
if (path.equals(DEFAULT_WEB_ASSET_DIR)) {
|
|
579
674
|
this.plugin.getBridge().setServerAssetPath(path);
|
|
580
675
|
} else {
|
|
581
676
|
this.plugin.getBridge().setServerBasePath(path);
|
|
@@ -584,7 +679,7 @@ public class LiveUpdate {
|
|
|
584
679
|
}
|
|
585
680
|
|
|
586
681
|
private void setCurrentCapacitorServerPathToDefaultWebAssetDir() {
|
|
587
|
-
setCurrentCapacitorServerPath(
|
|
682
|
+
setCurrentCapacitorServerPath(DEFAULT_WEB_ASSET_DIR);
|
|
588
683
|
}
|
|
589
684
|
|
|
590
685
|
private void setNextBundle(@NonNull String bundleId) {
|
|
@@ -598,7 +693,11 @@ public class LiveUpdate {
|
|
|
598
693
|
}
|
|
599
694
|
|
|
600
695
|
private void setNextCapacitorServerPathToDefaultWebAssetDir() {
|
|
601
|
-
setNextCapacitorServerPath(
|
|
696
|
+
setNextCapacitorServerPath(DEFAULT_WEB_ASSET_DIR);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private void setPreviousBundleId(@Nullable String bundleId) {
|
|
700
|
+
preferences.setPreviousBundleId(bundleId);
|
|
602
701
|
}
|
|
603
702
|
|
|
604
703
|
private void startRollbackTimer() {
|
|
@@ -617,6 +716,24 @@ public class LiveUpdate {
|
|
|
617
716
|
return destination;
|
|
618
717
|
}
|
|
619
718
|
|
|
719
|
+
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull PublicKey key) throws Exception {
|
|
720
|
+
try {
|
|
721
|
+
byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT);
|
|
722
|
+
Signature sig = Signature.getInstance("SHA256withRSA");
|
|
723
|
+
sig.initVerify(key);
|
|
724
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
725
|
+
Buffer buffer = new Buffer();
|
|
726
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
727
|
+
sig.update(buffer.readByteArray());
|
|
728
|
+
}
|
|
729
|
+
source.close();
|
|
730
|
+
return sig.verify(signatureBytes);
|
|
731
|
+
} catch (Exception exception) {
|
|
732
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
733
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
620
737
|
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
621
738
|
int lastVersionCode = preferences.getLastVersionCode();
|
|
622
739
|
int currentVersionCode = getPackageInfo().versionCode;
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package io.capawesome.capacitorjs.plugins.liveupdate;
|
|
2
2
|
|
|
3
3
|
import androidx.annotation.Nullable;
|
|
4
|
+
import java.security.Key;
|
|
4
5
|
|
|
5
6
|
public class LiveUpdateConfig {
|
|
6
7
|
|
|
@@ -9,6 +10,13 @@ public class LiveUpdateConfig {
|
|
|
9
10
|
|
|
10
11
|
private boolean autoDeleteBundles = false;
|
|
11
12
|
private boolean enabled = true;
|
|
13
|
+
|
|
14
|
+
@Nullable
|
|
15
|
+
private String location = null;
|
|
16
|
+
|
|
17
|
+
@Nullable
|
|
18
|
+
private String publicKey = null;
|
|
19
|
+
|
|
12
20
|
private int readyTimeout = 10000;
|
|
13
21
|
private boolean resetOnUpdate = true;
|
|
14
22
|
|
|
@@ -25,6 +33,16 @@ public class LiveUpdateConfig {
|
|
|
25
33
|
return enabled;
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
@Nullable
|
|
37
|
+
public String getLocation() {
|
|
38
|
+
return location;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Nullable
|
|
42
|
+
public String getPublicKey() {
|
|
43
|
+
return publicKey;
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
public int getReadyTimeout() {
|
|
29
47
|
return readyTimeout;
|
|
30
48
|
}
|
|
@@ -45,6 +63,14 @@ public class LiveUpdateConfig {
|
|
|
45
63
|
this.enabled = enabled;
|
|
46
64
|
}
|
|
47
65
|
|
|
66
|
+
public void setLocation(@Nullable String location) {
|
|
67
|
+
this.location = location;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public void setPublicKey(@Nullable String publicKey) {
|
|
71
|
+
this.publicKey = publicKey;
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
public void setReadyTimeout(int readyTimeout) {
|
|
49
75
|
this.readyTimeout = readyTimeout;
|
|
50
76
|
}
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -19,16 +19,21 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
|
19
19
|
public class LiveUpdatePlugin extends Plugin {
|
|
20
20
|
|
|
21
21
|
public static final String TAG = "LiveUpdate";
|
|
22
|
-
public static final String VERSION = "6.0.
|
|
22
|
+
public static final String VERSION = "6.2.0-dev.0a2a6a5.1722676620";
|
|
23
23
|
public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
|
|
24
24
|
public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
|
|
25
25
|
public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
|
|
26
26
|
public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
|
|
27
27
|
public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
|
|
28
28
|
public static final String ERROR_BUNDLE_NOT_FOUND = "bundle not found.";
|
|
29
|
+
public static final String ERROR_CHECKSUM_CALCULATION_FAILED = "Failed to calculate checksum.";
|
|
30
|
+
public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
|
|
29
31
|
public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
|
|
30
32
|
public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
|
|
31
33
|
public static final String ERROR_URL_MISSING = "url must be provided.";
|
|
34
|
+
public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
|
|
35
|
+
public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
|
|
36
|
+
public static final String ERROR_SIGNATURE_MISSING = "Bundle does not contain a signature.";
|
|
32
37
|
|
|
33
38
|
@Nullable
|
|
34
39
|
private LiveUpdateConfig config;
|
|
@@ -78,18 +83,19 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
78
83
|
@PluginMethod
|
|
79
84
|
public void downloadBundle(PluginCall call) {
|
|
80
85
|
try {
|
|
81
|
-
String url = call.getString("url");
|
|
82
|
-
if (url == null) {
|
|
83
|
-
call.reject(ERROR_URL_MISSING);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
86
|
String bundleId = call.getString("bundleId");
|
|
87
87
|
if (bundleId == null) {
|
|
88
88
|
call.reject(ERROR_BUNDLE_ID_MISSING);
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
|
+
String checksum = call.getString("checksum");
|
|
92
|
+
String url = call.getString("url");
|
|
93
|
+
if (url == null) {
|
|
94
|
+
call.reject(ERROR_URL_MISSING);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
91
97
|
|
|
92
|
-
DownloadBundleOptions options = new DownloadBundleOptions(bundleId, url);
|
|
98
|
+
DownloadBundleOptions options = new DownloadBundleOptions(bundleId, checksum, url);
|
|
93
99
|
EmptyCallback callback = new EmptyCallback() {
|
|
94
100
|
@Override
|
|
95
101
|
public void success() {
|
|
@@ -274,8 +280,20 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
274
280
|
@PluginMethod
|
|
275
281
|
public void ready(PluginCall call) {
|
|
276
282
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
|
|
284
|
+
@Override
|
|
285
|
+
public void success(Result result) {
|
|
286
|
+
call.resolve(result.toJSObject());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@Override
|
|
290
|
+
public void error(Exception exception) {
|
|
291
|
+
Logger.error(TAG, exception.getMessage(), exception);
|
|
292
|
+
call.reject(exception.getMessage());
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
implementation.ready(callback);
|
|
279
297
|
} catch (Exception exception) {
|
|
280
298
|
Logger.error(TAG, exception.getMessage(), exception);
|
|
281
299
|
call.reject(exception.getMessage());
|
|
@@ -427,6 +445,10 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
427
445
|
config.setAutoDeleteBundles(autoDeleteBundles);
|
|
428
446
|
boolean enabled = getConfig().getBoolean("enabled", config.getEnabled());
|
|
429
447
|
config.setEnabled(enabled);
|
|
448
|
+
String location = getConfig().getString("location", config.getLocation());
|
|
449
|
+
config.setLocation(location);
|
|
450
|
+
String publicKey = getConfig().getString("publicKey", config.getPublicKey());
|
|
451
|
+
config.setPublicKey(publicKey);
|
|
430
452
|
int readyTimeout = getConfig().getInt("readyTimeout", config.getReadyTimeout());
|
|
431
453
|
config.setReadyTimeout(readyTimeout);
|
|
432
454
|
boolean resetOnUpdate = getConfig().getBoolean("resetOnUpdate", config.getResetOnUpdate());
|
|
@@ -19,6 +19,7 @@ public class LiveUpdatePreferences {
|
|
|
19
19
|
private final String deviceIdKey = "deviceId"; // DO NOT CHANGE
|
|
20
20
|
private final String customIdKey = "customId"; // DO NOT CHANGE
|
|
21
21
|
private final String lastVersionCodeKey = "lastVersionCode"; // DO NOT CHANGE
|
|
22
|
+
private final String previousBundleIdKey = "previousBundleId"; // DO NOT CHANGE
|
|
22
23
|
|
|
23
24
|
public LiveUpdatePreferences(@NonNull Context context) {
|
|
24
25
|
this.context = context;
|
|
@@ -52,6 +53,13 @@ public class LiveUpdatePreferences {
|
|
|
52
53
|
return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getInt(lastVersionCodeKey, 0);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
@Nullable
|
|
57
|
+
public String getPreviousBundleId() {
|
|
58
|
+
return context
|
|
59
|
+
.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
|
|
60
|
+
.getString(previousBundleIdKey, null);
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
public void setChannel(@Nullable String channel) {
|
|
56
64
|
if (channel == null) {
|
|
57
65
|
settingsEditor.remove(channelKey);
|
|
@@ -83,4 +91,13 @@ public class LiveUpdatePreferences {
|
|
|
83
91
|
settingsEditor.putInt(lastVersionCodeKey, lastVersionCode);
|
|
84
92
|
settingsEditor.apply();
|
|
85
93
|
}
|
|
94
|
+
|
|
95
|
+
public void setPreviousBundleId(@Nullable String bundleId) {
|
|
96
|
+
if (bundleId == null) {
|
|
97
|
+
settingsEditor.remove(previousBundleIdKey);
|
|
98
|
+
} else {
|
|
99
|
+
settingsEditor.putString(previousBundleIdKey, bundleId);
|
|
100
|
+
}
|
|
101
|
+
settingsEditor.apply();
|
|
102
|
+
}
|
|
86
103
|
}
|
|
@@ -6,10 +6,29 @@ import org.json.JSONObject;
|
|
|
6
6
|
public class GetLatestBundleResponse {
|
|
7
7
|
|
|
8
8
|
private String bundleId;
|
|
9
|
+
|
|
10
|
+
@Nullable
|
|
11
|
+
private String checksum;
|
|
12
|
+
|
|
13
|
+
@Nullable
|
|
14
|
+
private String signature;
|
|
15
|
+
|
|
9
16
|
private String url;
|
|
10
17
|
|
|
11
18
|
public GetLatestBundleResponse(JSONObject responseJson) {
|
|
12
19
|
this.bundleId = responseJson.optString("bundleId");
|
|
20
|
+
String checksum = responseJson.optString("checksum", "null");
|
|
21
|
+
if (checksum.equals("null")) {
|
|
22
|
+
this.checksum = null;
|
|
23
|
+
} else {
|
|
24
|
+
this.checksum = checksum;
|
|
25
|
+
}
|
|
26
|
+
String signature = responseJson.optString("signature", "null");
|
|
27
|
+
if (signature.equals("null")) {
|
|
28
|
+
this.signature = null;
|
|
29
|
+
} else {
|
|
30
|
+
this.signature = signature;
|
|
31
|
+
}
|
|
13
32
|
this.url = responseJson.optString("url");
|
|
14
33
|
}
|
|
15
34
|
|
|
@@ -17,6 +36,16 @@ public class GetLatestBundleResponse {
|
|
|
17
36
|
return bundleId;
|
|
18
37
|
}
|
|
19
38
|
|
|
39
|
+
@Nullable
|
|
40
|
+
public String getChecksum() {
|
|
41
|
+
return checksum;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Nullable
|
|
45
|
+
public String getSignature() {
|
|
46
|
+
return signature;
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
public String getUrl() {
|
|
21
50
|
return url;
|
|
22
51
|
}
|