From 4368633479e09936f8c1d25841d0fa8f31cf88be Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Sat, 4 Apr 2020 16:54:36 +0300 Subject: [PATCH] Replace onPageStarted with shouldInterceptRequest --- .../webview/RNCWebViewManager.java | 275 +++++++++++++++++- 1 file changed, 264 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index fdcf90c..9cd700a 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -1,5 +1,27 @@ package com.reactnativecommunity.webview; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.OkHttpClient.Builder; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONException; +import org.json.JSONObject; +import java.net.HttpURLConnection; + + +import static okhttp3.internal.Util.UTF_8; + + +import android.util.Log; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.DownloadManager; @@ -30,6 +52,8 @@ import android.webkit.JavascriptInterface; import android.webkit.SslErrorHandler; import android.webkit.PermissionRequest; import android.webkit.URLUtil; +import android.webkit.ServiceWorkerController; +import android.webkit.ServiceWorkerClient; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; @@ -126,8 +150,10 @@ public class RNCWebViewManager extends SimpleViewManager { public static final int COMMAND_CLEAR_HISTORY = 1002; protected static final String REACT_CLASS = "RNCWebView"; + protected static final String HEADER_CONTENT_TYPE = "content-type"; protected static final String HTML_ENCODING = "UTF-8"; protected static final String HTML_MIME_TYPE = "text/html"; + protected static final String UNKNOWN_MIME_TYPE = "application/octet-stream"; protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView"; protected static final String HTTP_METHOD_POST = "POST"; // Use `webView.loadUrl("about:blank")` to reliably reset the view @@ -139,12 +165,22 @@ public class RNCWebViewManager extends SimpleViewManager { protected boolean mAllowsFullscreenVideo = false; protected @Nullable String mUserAgent = null; protected @Nullable String mUserAgentWithApplicationName = null; + protected static String userAgent; + + protected static OkHttpClient httpClient; public RNCWebViewManager() { mWebViewConfig = new WebViewConfig() { public void configWebView(WebView webView) { } }; + + + httpClient = new Builder() + .followRedirects(false) + .followSslRedirects(false) + .build(); + } public RNCWebViewManager(WebViewConfig webViewConfig) { @@ -171,6 +207,7 @@ public class RNCWebViewManager extends SimpleViewManager { @TargetApi(Build.VERSION_CODES.LOLLIPOP) protected WebView createViewInstance(ThemedReactContext reactContext) { RNCWebView webView = createRNCWebViewInstance(reactContext); + userAgent = webView.getSettings().getUserAgentString(); setupWebChromeClient(reactContext, webView); reactContext.addLifecycleEventListener(webView); mWebViewConfig.configWebView(webView); @@ -235,9 +272,102 @@ public class RNCWebViewManager extends SimpleViewManager { } }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + ServiceWorkerController swController = ServiceWorkerController.getInstance(); + swController.setServiceWorkerClient(new ServiceWorkerClient() { + @Override + public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { + Log.d(REACT_CLASS, "shouldInterceptRequest / ServiceWorkerClient"); + WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, false, webView); + if (response != null) { + Log.d(REACT_CLASS, "shouldInterceptRequest / ServiceWorkerClient -> return intersept response"); + return response; + } + + Log.d(REACT_CLASS, "shouldInterceptRequest / ServiceWorkerClient -> intercept response is nil, delegating up"); + return super.shouldInterceptRequest(request); + } + }); + } + return webView; } + private Boolean urlStringLooksInvalid(String urlString) { + return urlString == null || + urlString.trim().equals("") || + !(urlString.startsWith("http") && !urlString.startsWith("www")) || + urlString.contains("|"); + } + + private Boolean responseRequiresJSInjection(Response response) { + // we don't want to inject JS into redirects + if (response.isRedirect()) { + return false; + } + + // ...okhttp appends charset to content type sometimes, like "text/html; charset=UTF8" + final String contentTypeAndCharset = response.header(HEADER_CONTENT_TYPE, UNKNOWN_MIME_TYPE); + // ...and we only want to inject it in to HTML, really + return contentTypeAndCharset.startsWith(HTML_MIME_TYPE); + } + + + public WebResourceResponse shouldInterceptRequest(WebResourceRequest request, Boolean onlyMainFrame, RNCWebView webView) { + Uri url = request.getUrl(); + String urlStr = url.toString(); + + Log.i("StatusNativeLogs", "###shouldInterceptRequest 1"); + Log.d(REACT_CLASS, "new request "); + Log.d(REACT_CLASS, "url " + urlStr); + Log.d(REACT_CLASS, "host " + request.getUrl().getHost()); + Log.d(REACT_CLASS, "path " + request.getUrl().getPath()); + Log.d(REACT_CLASS, "main " + request.isForMainFrame()); + Log.d(REACT_CLASS, "headers " + request.getRequestHeaders().toString()); + Log.d(REACT_CLASS, "method " + request.getMethod()); + + Log.i("StatusNativeLogs", "###shouldInterceptRequest 2"); + if (onlyMainFrame && !request.isForMainFrame() || + urlStringLooksInvalid(urlStr)) { + return null;//super.shouldInterceptRequest(webView, request); + } + + Log.i("StatusNativeLogs", "###shouldInterceptRequest 3"); + try { + Log.i("StatusNativeLogs", "###shouldInterceptRequest 4"); + Request req = new Request.Builder() + .url(urlStr) + .header("User-Agent", userAgent) + .build(); + + Log.i("StatusNativeLogs", "### httpCall " + new Boolean(httpClient != null).toString()); + Response response = httpClient.newCall(req).execute(); + + Log.d(REACT_CLASS, "response headers " + response.headers().toString()); + Log.d(REACT_CLASS, "response code " + response.code()); + Log.d(REACT_CLASS, "response suc " + response.isSuccessful()); + + if (!responseRequiresJSInjection(response)) { + return null; + } + + InputStream is = response.body().byteStream(); + MediaType contentType = response.body().contentType(); + Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8; + + RNCWebView reactWebView = (RNCWebView) webView; + if (response.code() == HttpURLConnection.HTTP_OK) { + is = new InputStreamWithInjectedJS(is, reactWebView.injectedJSBeforeContentLoaded, charset); + } + + Log.d(REACT_CLASS, "inject our custom JS to this request"); + return new WebResourceResponse("text/html", charset.name(), is); + } catch (IOException e) { + return null; + } + } + @ReactProp(name = "javaScriptEnabled") public void setJavaScriptEnabled(WebView view, boolean enabled) { view.getSettings().setJavaScriptEnabled(enabled); @@ -740,8 +870,95 @@ public class RNCWebViewManager extends SimpleViewManager { } } - protected static class RNCWebViewClient extends WebViewClient { + public static class InputStreamWithInjectedJS extends InputStream { + private InputStream pageIS; + private InputStream scriptIS; + private Charset charset; + private static final String REACT_CLASS = "InputStreamWithInjectedJS"; + private static Map script = new HashMap<>(); + private boolean hasJS = false; + private boolean headWasFound = false; + private boolean scriptWasInjected = false; + private StringBuffer contentBuffer = new StringBuffer(); + + private static Charset getCharset(String charsetName) { + Charset cs = StandardCharsets.UTF_8; + try { + if (charsetName != null) { + cs = Charset.forName(charsetName); + } + } catch (UnsupportedCharsetException e) { + Log.d(REACT_CLASS, "wrong charset: " + charsetName); + } + + return cs; + } + + private static InputStream getScript(Charset charset) { + String js = script.get(charset); + if (js == null) { + String defaultJs = script.get(StandardCharsets.UTF_8); + js = new String(defaultJs.getBytes(StandardCharsets.UTF_8), charset); + script.put(charset, js); + } + + return new ByteArrayInputStream(js.getBytes(charset)); + } + + InputStreamWithInjectedJS(InputStream is, String js, Charset charset) { + if (js == null) { + this.pageIS = is; + } else { + this.hasJS = true; + this.charset = charset; + Charset cs = StandardCharsets.UTF_8; + String jsScript = ""; + script.put(cs, jsScript); + this.pageIS = is; + } + } + + @Override + public int read() throws IOException { + if (scriptWasInjected || !hasJS) { + return pageIS.read(); + } + + if (!scriptWasInjected && headWasFound) { + int nextByte = scriptIS.read(); + if (nextByte == -1) { + scriptIS.close(); + scriptWasInjected = true; + return pageIS.read(); + } else { + return nextByte; + } + } + + if (!headWasFound) { + int nextByte = pageIS.read(); + contentBuffer.append((char) nextByte); + int bufferLength = contentBuffer.length(); + if (nextByte == 62 && bufferLength >= 6) { + if (contentBuffer.substring(bufferLength - 6).equals("")) { + this.scriptIS = getScript(this.charset); + headWasFound = true; + } + } + + return nextByte; + } + + return pageIS.read(); + } + + } + + protected class RNCWebViewClient extends WebViewClient { + + + protected static final String REACT_CLASS = "RNCWebViewClient"; protected boolean mLastLoadFailed = false; protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent; @@ -752,6 +969,7 @@ public class RNCWebViewManager extends SimpleViewManager { ignoreErrFailedForThisURL = url; } + @Override public void onPageFinished(WebView webView, String url) { super.onPageFinished(webView, url); @@ -770,9 +988,6 @@ public class RNCWebViewManager extends SimpleViewManager { super.onPageStarted(webView, url, favicon); mLastLoadFailed = false; - RNCWebView reactWebView = (RNCWebView) webView; - reactWebView.callInjectedJavaScriptBeforeContentLoaded(); - dispatchEvent( webView, new TopLoadingStartEvent( @@ -780,6 +995,48 @@ public class RNCWebViewManager extends SimpleViewManager { createWebViewEvent(webView, url))); } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) { + Log.d(REACT_CLASS, "shouldInterceptRequest / WebViewClient"); + WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, true, (RNCWebView)webView); + if (response != null) { + Log.d(REACT_CLASS, "shouldInterceptRequest / WebViewClient -> return intercept response"); + return response; + } + + Log.d(REACT_CLASS, "shouldInterceptRequest / WebViewClient -> intercept response is nil, delegating up"); + return super.shouldInterceptRequest(webView, request); + + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (request == null || view == null) { + return false; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + + /* + * In order to follow redirects properly, we return null in interceptRequest(). + * Doing this breaks the web3 injection on the resulting page, so we have to reload to + * make sure web3 is available. + * */ + + if (request.isForMainFrame() && request.isRedirect()) { + view.loadUrl(request.getUrl().toString()); + return true; + } + } + + /* + * API < 24: TODO: implement based on https://github.com/toshiapp/toshi-android-client/blob/f4840d3d24ff60223662eddddceca8586a1be8bb/app/src/main/java/com/toshi/view/activity/webView/ToshiWebClient.kt#L99 + * */ + final String url = request.getUrl().toString(); + return this.shouldOverrideUrlLoading(view, url); + } + @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { progressChangedFilter.setWaitingForCommandLoadUrl(true); @@ -792,13 +1049,6 @@ public class RNCWebViewManager extends SimpleViewManager { } - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - final String url = request.getUrl().toString(); - return this.shouldOverrideUrlLoading(view, url); - } - @Override public void onReceivedSslError(final WebView webView, final SslErrorHandler handler, final SslError error) { handler.cancel(); @@ -957,6 +1207,9 @@ public class RNCWebViewManager extends SimpleViewManager { @Override public boolean onConsoleMessage(ConsoleMessage message) { + Log.i("StatusNativeLogs", "###js " + message.message() + " -- From line " + + message.lineNumber() + " of " + + message.sourceId()); if (ReactBuildConfig.DEBUG) { return super.onConsoleMessage(message); }