@protobase/mobile 0.45.1 → 0.45.3
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/android/build.gradle +34 -0
- package/android/src/liveupdate/java/net/protobase/mobile/GuardedLiveUpdate.java +304 -0
- package/android/src/main/java/net/protobase/mobile/BundlesFolder.java +46 -0
- package/android/src/main/java/net/protobase/mobile/GuardedWebViewClient.java +1 -16
- package/android/src/main/java/net/protobase/mobile/LocalServerGuard.java +1 -5
- package/android/src/main/java/net/protobase/mobile/Refusal.java +4 -0
- package/package.json +3 -2
package/android/build.gradle
CHANGED
|
@@ -13,6 +13,9 @@ buildscript {
|
|
|
13
13
|
|
|
14
14
|
apply plugin: 'com.android.library'
|
|
15
15
|
|
|
16
|
+
// The live updater's project, present only in an app that includes it (capacitor.settings.gradle lists every plugin).
|
|
17
|
+
def liveUpdate = findProject(':capawesome-capacitor-live-update')
|
|
18
|
+
|
|
16
19
|
android {
|
|
17
20
|
namespace = "net.protobase.mobile"
|
|
18
21
|
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
@@ -31,6 +34,11 @@ android {
|
|
|
31
34
|
lintOptions {
|
|
32
35
|
abortOnError = false
|
|
33
36
|
}
|
|
37
|
+
// GuardedLiveUpdate extends the live updater's plugin, so it compiles only beside it. Outside src/main, the Capacitor
|
|
38
|
+
// CLI never lists it on its own (@capacitor/cli android/update.js:63).
|
|
39
|
+
if (liveUpdate != null) {
|
|
40
|
+
sourceSets.main.java.srcDirs += 'src/liveupdate/java'
|
|
41
|
+
}
|
|
34
42
|
compileOptions {
|
|
35
43
|
sourceCompatibility JavaVersion.VERSION_21
|
|
36
44
|
targetCompatibility JavaVersion.VERSION_21
|
|
@@ -44,4 +52,30 @@ repositories {
|
|
|
44
52
|
|
|
45
53
|
dependencies {
|
|
46
54
|
implementation project(':capacitor-android')
|
|
55
|
+
if (liveUpdate != null) {
|
|
56
|
+
implementation liveUpdate
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The plugin list the app loads must match what this build compiles, however the build was started (protobase mobile,
|
|
61
|
+
// gradlew, Android Studio, after an `npx cap sync`): Capacitor loads no plugin at all when one listed class is missing
|
|
62
|
+
// (PluginManager.java:25-36, BridgeActivity.java:36-40), which would drop every Protobase stand-in, and the stock live
|
|
63
|
+
// updater listed hands page script all its methods. The CLI swaps the stock class for GuardedLiveUpdate from the same
|
|
64
|
+
// list the source set above follows.
|
|
65
|
+
def pluginList = rootProject.file('app/src/main/assets/capacitor.plugins.json')
|
|
66
|
+
def checkPluginList = tasks.register('checkProtobasePluginList') {
|
|
67
|
+
inputs.file(pluginList)
|
|
68
|
+
def compilesGuard = liveUpdate != null
|
|
69
|
+
doLast {
|
|
70
|
+
def entries = new groovy.json.JsonSlurper().parse(pluginList)
|
|
71
|
+
entries.each { entry ->
|
|
72
|
+
if (entry.pkg == '@capawesome/capacitor-live-update' || entry.classpath == 'io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin') {
|
|
73
|
+
throw new GradleException("${pluginList} lists ${entry.classpath}, which hands page script the live updater unguarded: run protobase mobile again instead of cap sync")
|
|
74
|
+
}
|
|
75
|
+
if (entry.classpath == 'net.protobase.mobile.GuardedLiveUpdate' && !compilesGuard) {
|
|
76
|
+
throw new GradleException("${pluginList} lists ${entry.classpath}, which this app does not compile without the live updater: run protobase mobile again")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
47
80
|
}
|
|
81
|
+
tasks.matching { it.name == 'preBuild' }.configureEach { dependsOn checkPluginList }
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
package net.protobase.mobile;
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.JSObject;
|
|
4
|
+
import com.getcapacitor.Logger;
|
|
5
|
+
import com.getcapacitor.Plugin;
|
|
6
|
+
import com.getcapacitor.PluginCall;
|
|
7
|
+
import com.getcapacitor.PluginConfig;
|
|
8
|
+
import com.getcapacitor.PluginMethod;
|
|
9
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
10
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin;
|
|
11
|
+
import java.lang.reflect.Method;
|
|
12
|
+
import java.net.URI;
|
|
13
|
+
import java.net.URISyntaxException;
|
|
14
|
+
import java.util.ArrayList;
|
|
15
|
+
import java.util.Arrays;
|
|
16
|
+
import java.util.Iterator;
|
|
17
|
+
import java.util.List;
|
|
18
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
19
|
+
import org.json.JSONObject;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Takes the stock LiveUpdate plugin's (8.7.1) place, so page script cannot steer the live updater. The CLI lists this
|
|
23
|
+
* class in capacitor.plugins.json instead of the stock one: listed beside it, two instances would each run their own
|
|
24
|
+
* rollback timer (LiveUpdate.java:147-171). Capacitor reaches only the methods a class's own overrides annotate
|
|
25
|
+
* (PluginHandle.java:145-157), so this subclass decides what page script reaches and keeps the stock lifecycle.
|
|
26
|
+
*
|
|
27
|
+
* - Every reachable method is declared here or by Capacitor's Plugin; load() refuses to load otherwise, so a plugin
|
|
28
|
+
* upgrade that adds a method leaves no LiveUpdate at all rather than an open one.
|
|
29
|
+
* - The methods the shell needs that take no input clear what page script passed before the stock code reads it.
|
|
30
|
+
* - downloadBundle takes only a manifest from the app's own releases URL, `releasesUrl + "/" + bundleId`, over https
|
|
31
|
+
* or loopback http, with `publicKey` pinned, so the stock code checks each file's signature (LiveUpdate.java:1633-1654)
|
|
32
|
+
* and a caller's checksum and signature are dropped. A few per run of the app, so a loop cannot drain the data plan.
|
|
33
|
+
* - Ids are a single safe component naming a direct child of the bundles folder: the stock code joins them to its path
|
|
34
|
+
* unchecked (LiveUpdate.java:665-667,1299-1302).
|
|
35
|
+
* - Everything else is refused: deleting, resetting, the vendor's cloud, config, channel and ids.
|
|
36
|
+
* - load() also refuses background updates, which sync with the vendor's cloud outside every method here
|
|
37
|
+
* (LiveUpdate.java:420-433), and needs `autoDeleteBundles` and a `readyTimeout`, so ready() drops every bundle but the
|
|
38
|
+
* current and next one and a bundle that never gets ready rolls back: page script cannot pin an older one.
|
|
39
|
+
*
|
|
40
|
+
* The config comes from capacitor.config.json, which ships in the binary outside the web part, beyond page script and
|
|
41
|
+
* a live update. This source set compiles only when the app has the plugin's Gradle project, which the CLI's swap and
|
|
42
|
+
* the build's plugin-list check take from the same list (@capacitor/cli android/update.js:20-36).
|
|
43
|
+
*/
|
|
44
|
+
@CapacitorPlugin(name = "LiveUpdate")
|
|
45
|
+
public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
46
|
+
|
|
47
|
+
private static final int MAX_DOWNLOADS = 3;
|
|
48
|
+
private static final AtomicInteger downloads = new AtomicInteger();
|
|
49
|
+
|
|
50
|
+
@Override
|
|
51
|
+
public void load() {
|
|
52
|
+
for (Method method : getClass().getMethods()) {
|
|
53
|
+
if (!method.isAnnotationPresent(PluginMethod.class)) continue;
|
|
54
|
+
Class<?> declaring = method.getDeclaringClass();
|
|
55
|
+
if (declaring == GuardedLiveUpdate.class || declaring == Plugin.class) continue;
|
|
56
|
+
refuseToLoad("LiveUpdate." + method.getName() + " has no guard in this Protobase app");
|
|
57
|
+
}
|
|
58
|
+
PluginConfig config = getConfig();
|
|
59
|
+
if (!"none".equals(config.getString("autoUpdateStrategy", "none"))) {
|
|
60
|
+
refuseToLoad("plugins.LiveUpdate.autoUpdateStrategy must be \"none\" in a Protobase app");
|
|
61
|
+
}
|
|
62
|
+
if (!config.getBoolean("autoDeleteBundles", false)) {
|
|
63
|
+
refuseToLoad("plugins.LiveUpdate.autoDeleteBundles must be true in a Protobase app");
|
|
64
|
+
}
|
|
65
|
+
if (config.getInt("readyTimeout", 0) <= 0) {
|
|
66
|
+
refuseToLoad("plugins.LiveUpdate.readyTimeout must be above 0 in a Protobase app");
|
|
67
|
+
}
|
|
68
|
+
super.load();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Capacitor then leaves the plugin unregistered, logging only that it failed to load (PluginHandle.java:98-109), so
|
|
72
|
+
// the reason is logged here.
|
|
73
|
+
private static void refuseToLoad(String reason) {
|
|
74
|
+
Logger.error(reason);
|
|
75
|
+
throw new IllegalStateException(reason);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Guarded
|
|
79
|
+
|
|
80
|
+
@Override
|
|
81
|
+
@PluginMethod
|
|
82
|
+
public void downloadBundle(PluginCall call) {
|
|
83
|
+
String releasesUrl = getConfig().getString("releasesUrl");
|
|
84
|
+
String publicKey = getConfig().getString("publicKey");
|
|
85
|
+
if (!ownSource(releasesUrl) || publicKey == null || publicKey.trim().isEmpty()) {
|
|
86
|
+
Refusal.rejectMethod(call, "LiveUpdate", "downloadBundle");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
String bundleId = call.getString("bundleId");
|
|
90
|
+
boolean allowed =
|
|
91
|
+
"manifest".equals(call.getString("artifactType")) &&
|
|
92
|
+
BundlesFolder.isBundleId(BundlesFolder.of(getContext()), bundleId) &&
|
|
93
|
+
(releasesUrl + "/" + bundleId).equals(call.getString("url"));
|
|
94
|
+
if (!allowed) {
|
|
95
|
+
Refusal.rejectMethod(call, "LiveUpdate", "downloadBundle");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (downloads.incrementAndGet() > MAX_DOWNLOADS) {
|
|
99
|
+
call.reject("LiveUpdate.downloadBundle was called too often in this run of the app");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
JSObject data = clear(call);
|
|
103
|
+
data.put("artifactType", "manifest");
|
|
104
|
+
data.put("bundleId", bundleId);
|
|
105
|
+
data.put("url", releasesUrl + "/" + bundleId);
|
|
106
|
+
super.downloadBundle(call);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Override
|
|
110
|
+
@PluginMethod
|
|
111
|
+
public void setNextBundle(PluginCall call) {
|
|
112
|
+
Object bundleId = call.getData().opt("bundleId");
|
|
113
|
+
if (bundleId == null || bundleId == JSONObject.NULL) {
|
|
114
|
+
// The stock code resets to the built-in bundle.
|
|
115
|
+
clear(call);
|
|
116
|
+
super.setNextBundle(call);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!(bundleId instanceof String) || !BundlesFolder.isBundleId(BundlesFolder.of(getContext()), (String) bundleId)) {
|
|
120
|
+
Refusal.rejectMethod(call, "LiveUpdate", "setNextBundle");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
clear(call).put("bundleId", bundleId);
|
|
124
|
+
super.setNextBundle(call);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Allowed: no input
|
|
128
|
+
|
|
129
|
+
@Override
|
|
130
|
+
@PluginMethod
|
|
131
|
+
public void ready(PluginCall call) {
|
|
132
|
+
clear(call);
|
|
133
|
+
super.ready(call);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Override
|
|
137
|
+
@PluginMethod
|
|
138
|
+
public void reload(PluginCall call) {
|
|
139
|
+
clear(call);
|
|
140
|
+
super.reload(call);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Override
|
|
144
|
+
@PluginMethod
|
|
145
|
+
public void getCurrentBundle(PluginCall call) {
|
|
146
|
+
clear(call);
|
|
147
|
+
super.getCurrentBundle(call);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Override
|
|
151
|
+
@PluginMethod
|
|
152
|
+
public void getNextBundle(PluginCall call) {
|
|
153
|
+
clear(call);
|
|
154
|
+
super.getNextBundle(call);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@Override
|
|
158
|
+
@PluginMethod
|
|
159
|
+
public void getDownloadedBundles(PluginCall call) {
|
|
160
|
+
clear(call);
|
|
161
|
+
super.getDownloadedBundles(call);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Override
|
|
165
|
+
@PluginMethod
|
|
166
|
+
public void getVersionCode(PluginCall call) {
|
|
167
|
+
clear(call);
|
|
168
|
+
super.getVersionCode(call);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Override
|
|
172
|
+
@PluginMethod
|
|
173
|
+
public void getVersionName(PluginCall call) {
|
|
174
|
+
clear(call);
|
|
175
|
+
super.getVersionName(call);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Refused
|
|
179
|
+
|
|
180
|
+
@Override
|
|
181
|
+
@PluginMethod
|
|
182
|
+
public void clearBlockedBundles(PluginCall call) {
|
|
183
|
+
Refusal.rejectMethod(call, "LiveUpdate", "clearBlockedBundles");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Override
|
|
187
|
+
@PluginMethod
|
|
188
|
+
public void deleteBundle(PluginCall call) {
|
|
189
|
+
Refusal.rejectMethod(call, "LiveUpdate", "deleteBundle");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@Override
|
|
193
|
+
@PluginMethod
|
|
194
|
+
public void fetchChannels(PluginCall call) {
|
|
195
|
+
Refusal.rejectMethod(call, "LiveUpdate", "fetchChannels");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@Override
|
|
199
|
+
@PluginMethod
|
|
200
|
+
public void fetchLatestBundle(PluginCall call) {
|
|
201
|
+
Refusal.rejectMethod(call, "LiveUpdate", "fetchLatestBundle");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@Override
|
|
205
|
+
@PluginMethod
|
|
206
|
+
public void getBlockedBundles(PluginCall call) {
|
|
207
|
+
Refusal.rejectMethod(call, "LiveUpdate", "getBlockedBundles");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@Override
|
|
211
|
+
@PluginMethod
|
|
212
|
+
public void getBundles(PluginCall call) {
|
|
213
|
+
Refusal.rejectMethod(call, "LiveUpdate", "getBundles");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Override
|
|
217
|
+
@PluginMethod
|
|
218
|
+
public void getChannel(PluginCall call) {
|
|
219
|
+
Refusal.rejectMethod(call, "LiveUpdate", "getChannel");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@Override
|
|
223
|
+
@PluginMethod
|
|
224
|
+
public void getConfig(PluginCall call) {
|
|
225
|
+
Refusal.rejectMethod(call, "LiveUpdate", "getConfig");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@Override
|
|
229
|
+
@PluginMethod
|
|
230
|
+
public void getCustomId(PluginCall call) {
|
|
231
|
+
Refusal.rejectMethod(call, "LiveUpdate", "getCustomId");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@Override
|
|
235
|
+
@PluginMethod
|
|
236
|
+
public void getDeviceId(PluginCall call) {
|
|
237
|
+
Refusal.rejectMethod(call, "LiveUpdate", "getDeviceId");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@Override
|
|
241
|
+
@PluginMethod
|
|
242
|
+
public void isSyncing(PluginCall call) {
|
|
243
|
+
Refusal.rejectMethod(call, "LiveUpdate", "isSyncing");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Override
|
|
247
|
+
@PluginMethod
|
|
248
|
+
public void reset(PluginCall call) {
|
|
249
|
+
Refusal.rejectMethod(call, "LiveUpdate", "reset");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@Override
|
|
253
|
+
@PluginMethod
|
|
254
|
+
public void resetConfig(PluginCall call) {
|
|
255
|
+
Refusal.rejectMethod(call, "LiveUpdate", "resetConfig");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Override
|
|
259
|
+
@PluginMethod
|
|
260
|
+
public void setChannel(PluginCall call) {
|
|
261
|
+
Refusal.rejectMethod(call, "LiveUpdate", "setChannel");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@Override
|
|
265
|
+
@PluginMethod
|
|
266
|
+
public void setConfig(PluginCall call) {
|
|
267
|
+
Refusal.rejectMethod(call, "LiveUpdate", "setConfig");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Override
|
|
271
|
+
@PluginMethod
|
|
272
|
+
public void setCustomId(PluginCall call) {
|
|
273
|
+
Refusal.rejectMethod(call, "LiveUpdate", "setCustomId");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@Override
|
|
277
|
+
@PluginMethod
|
|
278
|
+
public void sync(PluginCall call) {
|
|
279
|
+
Refusal.rejectMethod(call, "LiveUpdate", "sync");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// https, or http to the device itself, where no one sits on the path; a bare origin and path only.
|
|
283
|
+
private static boolean ownSource(String releasesUrl) {
|
|
284
|
+
if (releasesUrl == null) return false;
|
|
285
|
+
URI uri;
|
|
286
|
+
try {
|
|
287
|
+
uri = new URI(releasesUrl);
|
|
288
|
+
} catch (URISyntaxException e) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (uri.getHost() == null || uri.getRawUserInfo() != null || uri.getRawQuery() != null || uri.getRawFragment() != null) return false;
|
|
292
|
+
if ("https".equals(uri.getScheme())) return true;
|
|
293
|
+
return "http".equals(uri.getScheme()) && Arrays.asList("localhost", "127.0.0.1", "[::1]").contains(uri.getHost());
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// The live data the stock code reads (PluginCall.java:150-152), emptied: keys collected first, then removed.
|
|
297
|
+
private static JSObject clear(PluginCall call) {
|
|
298
|
+
JSObject data = call.getData();
|
|
299
|
+
List<String> keys = new ArrayList<>();
|
|
300
|
+
for (Iterator<String> it = data.keys(); it.hasNext(); ) keys.add(it.next());
|
|
301
|
+
for (String key : keys) data.remove(key);
|
|
302
|
+
return data;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package net.protobase.mobile;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import java.io.File;
|
|
5
|
+
import java.io.IOException;
|
|
6
|
+
import java.util.regex.Pattern;
|
|
7
|
+
|
|
8
|
+
/** The folder the live updater keeps its bundles in, a fixed name below the app's files (LiveUpdate.java:114). */
|
|
9
|
+
final class BundlesFolder {
|
|
10
|
+
|
|
11
|
+
static final String NAME = "_capacitor_live_update_bundles";
|
|
12
|
+
|
|
13
|
+
// One path component: no separator, no `.` or `..`, no NUL.
|
|
14
|
+
private static final Pattern BUNDLE_ID = Pattern.compile("[A-Za-z0-9_-]{1,64}");
|
|
15
|
+
|
|
16
|
+
private BundlesFolder() {}
|
|
17
|
+
|
|
18
|
+
static File of(Context context) {
|
|
19
|
+
return new File(context.getFilesDir(), NAME);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Compares whole path components of the canonical paths, so a sibling such as `<bundles>_x` is not inside. One
|
|
23
|
+
// that cannot be made canonical is refused.
|
|
24
|
+
static boolean contains(File root, File file) {
|
|
25
|
+
String canonical;
|
|
26
|
+
String rootPath;
|
|
27
|
+
try {
|
|
28
|
+
canonical = file.getCanonicalPath();
|
|
29
|
+
rootPath = root.getCanonicalPath();
|
|
30
|
+
} catch (IOException e) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return canonical.equals(rootPath) || canonical.startsWith(rootPath + File.separator);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// An id that names a direct child of the folder once symlinks are resolved, so an entry linked elsewhere is refused.
|
|
37
|
+
static boolean isBundleId(File root, String id) {
|
|
38
|
+
if (id == null || !BUNDLE_ID.matcher(id).matches()) return false;
|
|
39
|
+
try {
|
|
40
|
+
File parent = new File(root, id).getCanonicalFile().getParentFile();
|
|
41
|
+
return parent != null && parent.getPath().equals(root.getCanonicalPath());
|
|
42
|
+
} catch (IOException e) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -7,7 +7,6 @@ import com.getcapacitor.Bridge;
|
|
|
7
7
|
import com.getcapacitor.BridgeWebViewClient;
|
|
8
8
|
import java.io.ByteArrayInputStream;
|
|
9
9
|
import java.io.File;
|
|
10
|
-
import java.io.IOException;
|
|
11
10
|
import java.util.Arrays;
|
|
12
11
|
import java.util.Collections;
|
|
13
12
|
import java.util.Objects;
|
|
@@ -50,21 +49,7 @@ final class GuardedWebViewClient extends BridgeWebViewClient {
|
|
|
50
49
|
if (path.startsWith(Bridge.CAPACITOR_FILE_START) || path.startsWith(Bridge.CAPACITOR_CONTENT_START)) return true;
|
|
51
50
|
if (Arrays.asList(path.split("/")).contains("..")) return true;
|
|
52
51
|
if (basePath == null || !new File(basePath).isAbsolute()) return false;
|
|
53
|
-
return !
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Compares whole path components of the canonical paths, so a sibling such as `<bundles>_x` is not inside. One
|
|
57
|
-
// that cannot be made canonical is refused.
|
|
58
|
-
private boolean inside(File file) {
|
|
59
|
-
String canonical;
|
|
60
|
-
String root;
|
|
61
|
-
try {
|
|
62
|
-
canonical = file.getCanonicalPath();
|
|
63
|
-
root = bundles.getCanonicalPath();
|
|
64
|
-
} catch (IOException e) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
return canonical.equals(root) || canonical.startsWith(root + File.separator);
|
|
52
|
+
return !BundlesFolder.contains(bundles, new File(basePath + path));
|
|
68
53
|
}
|
|
69
54
|
|
|
70
55
|
private static WebResourceResponse forbidden() {
|
|
@@ -2,7 +2,6 @@ package net.protobase.mobile;
|
|
|
2
2
|
|
|
3
3
|
import com.getcapacitor.Plugin;
|
|
4
4
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
5
|
-
import java.io.File;
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Puts GuardedWebViewClient in front of the app's local server. It has no methods: it only acts when it loads, which
|
|
@@ -12,11 +11,8 @@ import java.io.File;
|
|
|
12
11
|
@CapacitorPlugin(name = "ProtobaseLocalServerGuard")
|
|
13
12
|
public class LocalServerGuard extends Plugin {
|
|
14
13
|
|
|
15
|
-
// The folder the live updater keeps its bundles in, a fixed name below the app's files (LiveUpdate.java:114).
|
|
16
|
-
private static final String LIVE_UPDATE_BUNDLES = "_capacitor_live_update_bundles";
|
|
17
|
-
|
|
18
14
|
@Override
|
|
19
15
|
public void load() {
|
|
20
|
-
getBridge().setWebViewClient(new GuardedWebViewClient(getBridge(),
|
|
16
|
+
getBridge().setWebViewClient(new GuardedWebViewClient(getBridge(), BundlesFolder.of(getContext())));
|
|
21
17
|
}
|
|
22
18
|
}
|
|
@@ -10,4 +10,8 @@ final class Refusal {
|
|
|
10
10
|
static void reject(PluginCall call, String plugin) {
|
|
11
11
|
call.reject(plugin + " is not available to page script in a Protobase app");
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
static void rejectMethod(PluginCall call, String plugin, String method) {
|
|
15
|
+
call.reject(plugin + "." + method + " is not available to page script in a Protobase app");
|
|
16
|
+
}
|
|
13
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/mobile",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.3",
|
|
4
4
|
"description": "The Capacitor that protobase mobile builds a Protobase app's iOS and Android projects with, at fixed versions, and the native plugin that keeps page script away from the session; installed by the CLI on first use.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"LICENSE",
|
|
18
18
|
"android/src/main/",
|
|
19
|
+
"android/src/liveupdate/",
|
|
19
20
|
"android/build.gradle"
|
|
20
21
|
],
|
|
21
|
-
"nativeContract":
|
|
22
|
+
"nativeContract": 2,
|
|
22
23
|
"capacitor": {
|
|
23
24
|
"android": {
|
|
24
25
|
"src": "android"
|