@capgo/inappbrowser 8.2.0 → 8.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,11 +66,14 @@ import com.caverock.androidsvg.SVG;
66
66
  import com.caverock.androidsvg.SVGParseException;
67
67
  import com.getcapacitor.JSObject;
68
68
  import java.io.ByteArrayInputStream;
69
+ import java.io.ByteArrayOutputStream;
69
70
  import java.io.File;
70
71
  import java.io.IOException;
71
72
  import java.io.InputStream;
73
+ import java.net.HttpURLConnection;
72
74
  import java.net.URI;
73
75
  import java.net.URISyntaxException;
76
+ import java.net.URL;
74
77
  import java.nio.charset.StandardCharsets;
75
78
  import java.security.PrivateKey;
76
79
  import java.security.cert.X509Certificate;
@@ -85,7 +88,6 @@ import java.util.concurrent.ExecutorService;
85
88
  import java.util.concurrent.Executors;
86
89
  import java.util.concurrent.Semaphore;
87
90
  import java.util.concurrent.TimeUnit;
88
- import java.util.regex.Matcher;
89
91
  import java.util.regex.Pattern;
90
92
  import org.json.JSONException;
91
93
  import org.json.JSONObject;
@@ -96,6 +98,12 @@ public class WebViewDialog extends Dialog {
96
98
 
97
99
  private WebResourceResponse response;
98
100
  private final Semaphore semaphore;
101
+ // Non-null when request came via JS proxy bridge (_capgo_proxy_ rewrite).
102
+ // Used by handleProxyResponse to do a native pass-through fetch when
103
+ // the handler returns null, since the rewritten URL is not loadable.
104
+ private String bridgedOriginalUrl;
105
+ private String bridgedMethod;
106
+ private Map<String, String> bridgedHeaders;
99
107
 
100
108
  public WebResourceResponse getResponse() {
101
109
  return response;
@@ -117,6 +125,9 @@ public class WebViewDialog extends Dialog {
117
125
  private final WebView capacitorWebView;
118
126
  private String instanceId = "";
119
127
  private final Map<String, ProxiedRequest> proxiedRequestsHashmap = new HashMap<>();
128
+ private ProxyBridge proxyBridge;
129
+ private String proxyBridgeScript;
130
+ private String proxyAccessToken;
120
131
  private final ExecutorService executorService = Executors.newCachedThreadPool();
121
132
  private int iconColor = Color.BLACK; // Default icon color
122
133
  private boolean isHiddenModeActive = false;
@@ -431,6 +442,13 @@ public class WebViewDialog extends Dialog {
431
442
  _webView.addJavascriptInterface(new JavaScriptInterface(), "mobileApp");
432
443
  _webView.addJavascriptInterface(new PreShowScriptInterface(), "PreShowScriptInterface");
433
444
  _webView.addJavascriptInterface(new PrintInterface(this._context, _webView), "PrintInterface");
445
+
446
+ if (_options.getProxyRequests()) {
447
+ proxyAccessToken = UUID.randomUUID().toString();
448
+ proxyBridge = new ProxyBridge(proxyAccessToken);
449
+ _webView.addJavascriptInterface(proxyBridge, "__capgoProxy");
450
+ proxyBridgeScript = loadProxyBridgeScript();
451
+ }
434
452
  _webView.getSettings().setJavaScriptEnabled(true);
435
453
  _webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
436
454
  _webView.getSettings().setDatabaseEnabled(true);
@@ -2182,74 +2200,6 @@ public class WebViewDialog extends Dialog {
2182
2200
  buttonNearDoneView.setColorFilter(iconColor);
2183
2201
  }
2184
2202
 
2185
- public void handleProxyResultError(String result, String id) {
2186
- Log.i("InAppBrowserProxy", String.format("handleProxyResultError: %s, ok: %s id: %s", result, false, id));
2187
- ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
2188
- if (proxiedRequest == null) {
2189
- Log.e("InAppBrowserProxy", "proxiedRequest is null");
2190
- return;
2191
- }
2192
- proxiedRequestsHashmap.remove(id);
2193
- proxiedRequest.semaphore.release();
2194
- }
2195
-
2196
- public void handleProxyResultOk(JSONObject result, String id) {
2197
- Log.i("InAppBrowserProxy", String.format("handleProxyResultOk: %s, ok: %s, id: %s", result, true, id));
2198
- ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
2199
- if (proxiedRequest == null) {
2200
- Log.e("InAppBrowserProxy", "proxiedRequest is null");
2201
- return;
2202
- }
2203
- proxiedRequestsHashmap.remove(id);
2204
-
2205
- if (result == null) {
2206
- proxiedRequest.semaphore.release();
2207
- return;
2208
- }
2209
-
2210
- Map<String, String> responseHeaders = new HashMap<>();
2211
- String body;
2212
- int code;
2213
-
2214
- try {
2215
- body = result.getString("body");
2216
- code = result.getInt("code");
2217
- JSONObject headers = result.getJSONObject("headers");
2218
- for (Iterator<String> it = headers.keys(); it.hasNext(); ) {
2219
- String headerName = it.next();
2220
- String header = headers.getString(headerName);
2221
- responseHeaders.put(headerName, header);
2222
- }
2223
- } catch (JSONException e) {
2224
- Log.e("InAppBrowserProxy", "Cannot parse OK result", e);
2225
- return;
2226
- }
2227
-
2228
- String contentType = responseHeaders.get("Content-Type");
2229
- if (contentType == null) {
2230
- contentType = responseHeaders.get("content-type");
2231
- }
2232
- if (contentType == null) {
2233
- Log.e("InAppBrowserProxy", "'Content-Type' header is required");
2234
- return;
2235
- }
2236
-
2237
- if (!((100 <= code && code <= 299) || (400 <= code && code <= 599))) {
2238
- Log.e("InAppBrowserProxy", String.format("Status code %s outside of the allowed range", code));
2239
- return;
2240
- }
2241
-
2242
- WebResourceResponse webResourceResponse = new WebResourceResponse(
2243
- contentType,
2244
- "utf-8",
2245
- new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))
2246
- );
2247
-
2248
- webResourceResponse.setStatusCodeAndReasonPhrase(code, getReasonPhrase(code));
2249
- proxiedRequest.response = webResourceResponse;
2250
- proxiedRequest.semaphore.release();
2251
- }
2252
-
2253
2203
  private void setWebViewClient() {
2254
2204
  _webView.setWebViewClient(
2255
2205
  new WebViewClient() {
@@ -2550,18 +2500,6 @@ public class WebViewDialog extends Dialog {
2550
2500
  }
2551
2501
  }
2552
2502
 
2553
- private String randomRequestId() {
2554
- return UUID.randomUUID().toString();
2555
- }
2556
-
2557
- private String toBase64(String raw) {
2558
- String s = Base64.encodeToString(raw.getBytes(), Base64.NO_WRAP);
2559
- if (s.endsWith("=")) {
2560
- s = s.substring(0, s.length() - 2);
2561
- }
2562
- return s;
2563
- }
2564
-
2565
2503
  //
2566
2504
  // void handleRedirect(String currentUrl, Response response) {
2567
2505
  // String loc = response.header("Location");
@@ -2573,104 +2511,98 @@ public class WebViewDialog extends Dialog {
2573
2511
  if (view == null || _webView == null) {
2574
2512
  return null;
2575
2513
  }
2576
- Pattern pattern = _options.getProxyRequestsPattern();
2577
- if (pattern == null) {
2578
- return null;
2579
- }
2580
- Matcher matcher = pattern.matcher(request.getUrl().toString());
2581
- if (!matcher.find()) {
2514
+ if (!_options.getProxyRequests()) {
2582
2515
  return null;
2583
2516
  }
2584
2517
 
2585
- // Requests matches the regex
2586
- if (Objects.equals(request.getMethod(), "POST")) {
2587
- // Log.e("HTTP", String.format("returned null (ok) %s", request.getUrl().toString()));
2588
- return null;
2589
- }
2518
+ String requestUrl = request.getUrl().toString();
2519
+ String originalUrl;
2520
+ String method;
2521
+ String headersJson;
2522
+ String base64Body;
2590
2523
 
2591
- Log.i("InAppBrowserProxy", String.format("Proxying request: %s", request.getUrl().toString()));
2524
+ if (requestUrl.contains("/_capgo_proxy_?")) {
2525
+ // JS-patched fetch/XHR: extract original URL and stored request data
2526
+ Uri uri = request.getUrl();
2527
+ originalUrl = uri.getQueryParameter("u");
2528
+ String requestId = uri.getQueryParameter("rid");
2529
+ if (originalUrl == null || requestId == null) {
2530
+ return null;
2531
+ }
2592
2532
 
2593
- // We need to call a JS function
2594
- String requestId = randomRequestId();
2595
- ProxiedRequest proxiedRequest = new ProxiedRequest();
2596
- addProxiedRequest(requestId, proxiedRequest);
2533
+ ProxyBridge.StoredRequest stored = proxyBridge != null ? proxyBridge.getAndRemove(requestId) : null;
2534
+ method = stored != null ? stored.method : "GET";
2535
+ headersJson = stored != null ? stored.headersJson : "{}";
2536
+ base64Body = stored != null ? stored.base64Body : "";
2537
+ } else {
2538
+ // Direct resource load (img, link, script, etc.) — extract from WebResourceRequest
2539
+ // Note: WebResourceRequest does not expose the request body, so for non-GET/HEAD
2540
+ // methods (e.g. form POST) the base64Body will be empty. This is an Android
2541
+ // platform limitation — the proxy handler receives the URL/headers/method but
2542
+ // must re-fetch the body if needed. In practice, direct resource loads from HTML
2543
+ // (img, link, script, iframe) are always GET.
2544
+ String scheme = request.getUrl().getScheme();
2545
+ if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
2546
+ return null;
2547
+ }
2597
2548
 
2598
- // lsuakdchgbbaHandleProxiedRequest
2599
- activity.runOnUiThread(
2600
- new Runnable() {
2601
- @Override
2602
- public void run() {
2603
- StringBuilder headers = new StringBuilder();
2604
- Map<String, String> requestHeaders = request.getRequestHeaders();
2605
- for (Map.Entry<String, String> header : requestHeaders.entrySet()) {
2606
- headers.append(
2607
- String.format("h[atob('%s')]=atob('%s');", toBase64(header.getKey()), toBase64(header.getValue()))
2608
- );
2549
+ originalUrl = requestUrl;
2550
+ method = request.getMethod();
2551
+
2552
+ // Convert request headers to JSON
2553
+ Map<String, String> reqHeaders = request.getRequestHeaders();
2554
+ JSONObject headersObj = new JSONObject();
2555
+ if (reqHeaders != null) {
2556
+ for (Map.Entry<String, String> entry : reqHeaders.entrySet()) {
2557
+ try {
2558
+ headersObj.put(entry.getKey(), entry.getValue());
2559
+ } catch (JSONException e) {
2560
+ // skip malformed header
2609
2561
  }
2610
- String jsTemplate = """
2611
- try {
2612
- function getHeaders() {
2613
- const h = {};
2614
- %s
2615
- return h;
2616
- }
2617
- window.InAppBrowserProxyRequest(new Request(atob('%s'), {
2618
- headers: getHeaders(),
2619
- method: '%s'
2620
- })).then(async (res) => {
2621
- Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
2622
- ok: true,
2623
- result: (!!res ? {
2624
- headers: Object.fromEntries(res.headers.entries()),
2625
- code: res.status,
2626
- body: (await res.text())
2627
- } : null),
2628
- id: '%s',
2629
- webviewId: '%s'
2630
- });
2631
- }).catch((e) => {
2632
- Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
2633
- ok: false,
2634
- result: e.toString(),
2635
- id: '%s',
2636
- webviewId: '%s'
2637
- });
2638
- });
2639
- } catch (e) {
2640
- Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
2641
- ok: false,
2642
- result: e.toString(),
2643
- id: '%s',
2644
- webviewId: '%s'
2645
- });
2646
- }
2647
- """;
2648
- String dialogId = instanceId != null ? instanceId : "";
2649
- String s = String.format(
2650
- jsTemplate,
2651
- headers,
2652
- toBase64(request.getUrl().toString()),
2653
- request.getMethod(),
2654
- requestId,
2655
- dialogId,
2656
- requestId,
2657
- dialogId,
2658
- requestId,
2659
- dialogId
2660
- );
2661
- // Log.i("HTTP", s);
2662
- capacitorWebView.evaluateJavascript(s, null);
2663
2562
  }
2664
2563
  }
2665
- );
2564
+ headersJson = headersObj.toString();
2565
+ base64Body = "";
2566
+ }
2567
+
2568
+ // Create a new requestId for the semaphore wait
2569
+ String proxyId = UUID.randomUUID().toString();
2570
+ ProxiedRequest proxiedRequest = new ProxiedRequest();
2571
+
2572
+ // For bridged requests, store original URL so null response
2573
+ // can do a native pass-through instead of loading the /_capgo_proxy_ URL
2574
+ if (requestUrl.contains("/_capgo_proxy_?")) {
2575
+ proxiedRequest.bridgedOriginalUrl = originalUrl;
2576
+ proxiedRequest.bridgedMethod = method;
2577
+ try {
2578
+ JSONObject hdr = new JSONObject(headersJson);
2579
+ Map<String, String> hdrMap = new HashMap<>();
2580
+ Iterator<String> keys = hdr.keys();
2581
+ while (keys.hasNext()) {
2582
+ String k = keys.next();
2583
+ hdrMap.put(k, hdr.getString(k));
2584
+ }
2585
+ proxiedRequest.bridgedHeaders = hdrMap;
2586
+ } catch (JSONException e) {
2587
+ proxiedRequest.bridgedHeaders = new HashMap<>();
2588
+ }
2589
+ }
2666
2590
 
2667
- // 10 seconds wait max
2591
+ addProxiedRequest(proxyId, proxiedRequest);
2592
+
2593
+ // Fire proxyRequest event to the Capacitor plugin
2594
+ String dialogId = instanceId != null ? instanceId : "";
2595
+ _options
2596
+ .getCallbacks()
2597
+ .proxyRequestEvent(proxyId, originalUrl, method, headersJson, base64Body.isEmpty() ? null : base64Body, dialogId);
2598
+
2599
+ // Wait for response (10 seconds max)
2668
2600
  try {
2669
2601
  if (proxiedRequest.semaphore.tryAcquire(1, 10, TimeUnit.SECONDS)) {
2670
2602
  return proxiedRequest.response;
2671
2603
  } else {
2672
- Log.e("InAppBrowserProxy", "Semaphore timed out");
2673
- removeProxiedRequest(requestId); // prevent mem leak
2604
+ Log.e("InAppBrowserProxy", "Semaphore timed out for: " + originalUrl);
2605
+ removeProxiedRequest(proxyId);
2674
2606
  }
2675
2607
  } catch (InterruptedException e) {
2676
2608
  Log.e("InAppBrowserProxy", "Semaphore wait error", e);
@@ -2756,6 +2688,13 @@ public class WebViewDialog extends Dialog {
2756
2688
  } catch (URISyntaxException e) {
2757
2689
  // Do nothing
2758
2690
  }
2691
+
2692
+ // Inject proxy bridge script early so fetch/XHR are patched before page JS runs
2693
+ if (_options.getProxyRequests() && proxyBridgeScript != null && proxyAccessToken != null) {
2694
+ // Embed the access token directly in the script to avoid exposing it on window
2695
+ String scriptWithToken = proxyBridgeScript.replace("___CAPGO_PROXY_TOKEN___", proxyAccessToken);
2696
+ view.evaluateJavascript(scriptWithToken, null);
2697
+ }
2759
2698
  }
2760
2699
 
2761
2700
  public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
@@ -3114,6 +3053,164 @@ public class WebViewDialog extends Dialog {
3114
3053
  }
3115
3054
  }
3116
3055
 
3056
+ private String loadProxyBridgeScript() {
3057
+ try (InputStream is = _context.getAssets().open("proxy-bridge.js")) {
3058
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
3059
+ byte[] buffer = new byte[4096];
3060
+ int bytesRead;
3061
+ while ((bytesRead = is.read(buffer)) != -1) {
3062
+ result.write(buffer, 0, bytesRead);
3063
+ }
3064
+ return result.toString(StandardCharsets.UTF_8.name());
3065
+ } catch (IOException e) {
3066
+ Log.e("InAppBrowserProxy", "Failed to load proxy-bridge.js", e);
3067
+ return null;
3068
+ }
3069
+ }
3070
+
3071
+ public void handleProxyResponse(String requestId, JSObject response) {
3072
+ ProxiedRequest proxiedRequest;
3073
+ synchronized (proxiedRequestsHashmap) {
3074
+ proxiedRequest = proxiedRequestsHashmap.get(requestId);
3075
+ }
3076
+ if (proxiedRequest == null) {
3077
+ Log.e("InAppBrowserProxy", "No pending request for id: " + requestId);
3078
+ return;
3079
+ }
3080
+
3081
+ if (response == null) {
3082
+ // null response = pass through
3083
+ if (proxiedRequest.bridgedOriginalUrl != null) {
3084
+ // Bridged fetch/XHR: URL was rewritten to /_capgo_proxy_, so WebView
3085
+ // can't load it directly. Do a native pass-through fetch instead.
3086
+ executeNativePassThrough(proxiedRequest);
3087
+ }
3088
+ // For direct resource loads the response stays null, which tells
3089
+ // shouldInterceptRequest to return null and let WebView load normally.
3090
+ synchronized (proxiedRequestsHashmap) {
3091
+ proxiedRequestsHashmap.remove(requestId);
3092
+ }
3093
+ proxiedRequest.semaphore.release();
3094
+ return;
3095
+ }
3096
+
3097
+ try {
3098
+ String base64Body = response.getString("body");
3099
+ int status = response.getInteger("status", 200);
3100
+ JSObject headers = response.getJSObject("headers");
3101
+
3102
+ Map<String, String> responseHeaders = new HashMap<>();
3103
+ if (headers != null) {
3104
+ Iterator<String> keys = headers.keys();
3105
+ while (keys.hasNext()) {
3106
+ String key = keys.next();
3107
+ responseHeaders.put(key, headers.getString(key));
3108
+ }
3109
+ }
3110
+
3111
+ byte[] bodyBytes = (base64Body != null && !base64Body.isEmpty()) ? Base64.decode(base64Body, Base64.DEFAULT) : new byte[0];
3112
+
3113
+ String contentType = responseHeaders.get("content-type");
3114
+ if (contentType == null) {
3115
+ contentType = responseHeaders.get("Content-Type");
3116
+ }
3117
+ if (contentType == null) {
3118
+ contentType = "application/octet-stream";
3119
+ }
3120
+
3121
+ if (status < 100 || status > 599) {
3122
+ Log.w("InAppBrowserProxy", "Invalid HTTP status " + status + ", defaulting to 200");
3123
+ status = 200;
3124
+ }
3125
+ String reasonPhrase = getReasonPhrase(status);
3126
+ if (reasonPhrase.isEmpty()) {
3127
+ reasonPhrase = "Unknown";
3128
+ }
3129
+
3130
+ WebResourceResponse webResourceResponse = new WebResourceResponse(contentType, "utf-8", new ByteArrayInputStream(bodyBytes));
3131
+ webResourceResponse.setStatusCodeAndReasonPhrase(status, reasonPhrase);
3132
+ webResourceResponse.setResponseHeaders(responseHeaders);
3133
+
3134
+ proxiedRequest.response = webResourceResponse;
3135
+ } catch (Exception e) {
3136
+ Log.e("InAppBrowserProxy", "Error building proxy response", e);
3137
+ }
3138
+
3139
+ synchronized (proxiedRequestsHashmap) {
3140
+ proxiedRequestsHashmap.remove(requestId);
3141
+ }
3142
+ proxiedRequest.semaphore.release();
3143
+ }
3144
+
3145
+ /**
3146
+ * Performs a native HTTP request for bridged fetch/XHR pass-through.
3147
+ * Called when proxy handler returns null for a request whose URL was
3148
+ * rewritten to /_capgo_proxy_, so WebView can't load it directly.
3149
+ */
3150
+ private void executeNativePassThrough(ProxiedRequest proxiedRequest) {
3151
+ try {
3152
+ URL url = new URL(proxiedRequest.bridgedOriginalUrl);
3153
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
3154
+ conn.setRequestMethod(proxiedRequest.bridgedMethod != null ? proxiedRequest.bridgedMethod : "GET");
3155
+ conn.setInstanceFollowRedirects(true);
3156
+
3157
+ if (proxiedRequest.bridgedHeaders != null) {
3158
+ for (Map.Entry<String, String> entry : proxiedRequest.bridgedHeaders.entrySet()) {
3159
+ conn.setRequestProperty(entry.getKey(), entry.getValue());
3160
+ }
3161
+ }
3162
+
3163
+ int status = conn.getResponseCode();
3164
+ InputStream inputStream;
3165
+ try {
3166
+ inputStream = conn.getInputStream();
3167
+ } catch (IOException e) {
3168
+ inputStream = conn.getErrorStream();
3169
+ }
3170
+
3171
+ byte[] bodyBytes = new byte[0];
3172
+ if (inputStream != null) {
3173
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
3174
+ byte[] buf = new byte[4096];
3175
+ int n;
3176
+ while ((n = inputStream.read(buf)) != -1) {
3177
+ baos.write(buf, 0, n);
3178
+ }
3179
+ inputStream.close();
3180
+ bodyBytes = baos.toByteArray();
3181
+ }
3182
+
3183
+ Map<String, String> responseHeaders = new HashMap<>();
3184
+ for (Map.Entry<String, java.util.List<String>> entry : conn.getHeaderFields().entrySet()) {
3185
+ if (entry.getKey() != null && entry.getValue() != null && !entry.getValue().isEmpty()) {
3186
+ responseHeaders.put(entry.getKey(), entry.getValue().get(0));
3187
+ }
3188
+ }
3189
+
3190
+ String contentType = responseHeaders.get("Content-Type");
3191
+ if (contentType == null) {
3192
+ contentType = responseHeaders.get("content-type");
3193
+ }
3194
+ if (contentType == null) {
3195
+ contentType = "application/octet-stream";
3196
+ }
3197
+
3198
+ String reasonPhrase = getReasonPhrase(status);
3199
+ if (reasonPhrase.isEmpty()) {
3200
+ reasonPhrase = "Unknown";
3201
+ }
3202
+
3203
+ WebResourceResponse webResourceResponse = new WebResourceResponse(contentType, "utf-8", new ByteArrayInputStream(bodyBytes));
3204
+ webResourceResponse.setStatusCodeAndReasonPhrase(status, reasonPhrase);
3205
+ webResourceResponse.setResponseHeaders(responseHeaders);
3206
+ proxiedRequest.response = webResourceResponse;
3207
+
3208
+ conn.disconnect();
3209
+ } catch (Exception e) {
3210
+ Log.e("InAppBrowserProxy", "Native pass-through failed for: " + proxiedRequest.bridgedOriginalUrl, e);
3211
+ }
3212
+ }
3213
+
3117
3214
  private void shareUrl() {
3118
3215
  Intent shareIntent = new Intent(Intent.ACTION_SEND);
3119
3216
  shareIntent.setType("text/plain");