diff --git a/android/src/main/java/com/reactnativecommunity/webview/InputStreamWithInjectedJS.java b/android/src/main/java/com/reactnativecommunity/webview/InputStreamWithInjectedJS.java new file mode 100644 index 0000000000..a54179bea3 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/webview/InputStreamWithInjectedJS.java @@ -0,0 +1,101 @@ +package com.reactnativecommunity.webview; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.RequiresApi; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.*; +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class InputStreamWithInjectedJS extends InputStream { + private InputStream pageIS; + private InputStream scriptIS; + private Charset charset; + private static final String TAG = "InputStreamWithInjectedJS"; + private static Map script = new HashMap<>(); + private boolean hasJS = false; + private boolean headWasFound = false; + private boolean scriptWasInjected = false; + private boolean openingHeadFound = false; + private StringBuffer contentBuffer = new StringBuffer(); + private static Charset getCharset(String charsetName) { + Charset cs = UTF_8; + try { + if (charsetName != null) { + cs = Charset.forName(charsetName); + } + } catch (UnsupportedCharsetException e) { + Log.d("CustomWebview", "wrong charset: " + charsetName); + } + return cs; + } + private static InputStream getScript(Charset charset) { + String js = script.get(charset); + if (js == null) { + String defaultJs = script.get(UTF_8); + js = new String(defaultJs.getBytes(UTF_8), charset); + script.put(charset, js); + } + return new ByteArrayInputStream(js.getBytes(charset)); + } + InputStreamWithInjectedJS(InputStream is, String js, Charset charset, Context c) { + if (js == null) { + this.pageIS = is; + } else { + this.hasJS = true; + this.charset = charset; + Charset cs = 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(); + char nextByteStr = (char) nextByte; + contentBuffer.append(nextByteStr); + int bufferLength = contentBuffer.length(); + String headString = "= 5) { + String stringToMatch = contentBuffer.substring(bufferLength - 5).toLowerCase(); + if (stringToMatch.contains(headString)) { + openingHeadFound = true; + } + } + } + return nextByte; + } + return pageIS.read(); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index 702c1a427c..6592f50673 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -27,10 +27,13 @@ import android.webkit.GeolocationPermissions; import android.webkit.JavascriptInterface; import android.webkit.PermissionRequest; +import android.webkit.ServiceWorkerClient; +import android.webkit.ServiceWorkerController; import android.webkit.URLUtil; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -66,10 +69,14 @@ import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; @@ -77,6 +84,13 @@ import javax.annotation.Nullable; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.OkHttpClient.Builder; +import static okhttp3.internal.Util.UTF_8; + /** * Manages instances of {@link WebView} *

@@ -113,6 +127,9 @@ public class RNCWebViewManager extends SimpleViewManager { public static final int COMMAND_LOAD_URL = 7; public static final int COMMAND_FOCUS = 8; protected static final String REACT_CLASS = "RNCWebView"; + protected final static String HEADER_CONTENT_TYPE = "content-type"; + protected static final String MIME_TEXT_HTML = "text/html"; + protected static final String MIME_UNKNOWN = "application/octet-stream"; protected static final String HTML_ENCODING = "UTF-8"; protected static final String HTML_MIME_TYPE = "text/html"; protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView"; @@ -121,6 +138,7 @@ public class RNCWebViewManager extends SimpleViewManager { // state and release page resources (including any running JavaScript). protected static final String BLANK_URL = "about:blank"; protected WebViewConfig mWebViewConfig; + private OkHttpClient httpClient; protected RNCWebChromeClient mWebChromeClient = null; protected boolean mAllowsFullscreenVideo = false; @@ -128,6 +146,12 @@ public class RNCWebViewManager extends SimpleViewManager { protected @Nullable String mUserAgentWithApplicationName = null; public RNCWebViewManager() { + Builder b = new Builder(); + httpClient = b + .followRedirects(false) + .followSslRedirects(false) + .build(); + mWebViewConfig = new WebViewConfig() { public void configWebView(WebView webView) { } @@ -150,6 +174,54 @@ public String getName() { return REACT_CLASS; } + public static Boolean urlStringLooksInvalid(String urlString) { + return urlString == null || + urlString.trim().equals("") || + !(urlString.startsWith("http") && !urlString.startsWith("www")) || + urlString.contains("|"); + } + + public static Boolean responseRequiresJSInjection(Response response) { + if (response.isRedirect()) { + return false; + } + final String contentTypeAndCharset = response.header(HEADER_CONTENT_TYPE, MIME_UNKNOWN); + return contentTypeAndCharset.startsWith(MIME_TEXT_HTML); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public WebResourceResponse shouldInterceptRequest(WebResourceRequest request, Boolean onlyMainFrame, RNCWebView webView) { + Uri url = request.getUrl(); + String urlStr = url.toString(); + if (onlyMainFrame && !request.isForMainFrame()) { + return null; + } + if (RNCWebViewManager.urlStringLooksInvalid(urlStr)) { + return null; + } + try { + + + Request req = new Request.Builder() + .header("User-Agent", mUserAgent) + .url(urlStr) + .build(); + Response response = httpClient.newCall(req).execute(); + if (!RNCWebViewManager.responseRequiresJSInjection(response)) { + return null; + } + InputStream is = response.body().byteStream(); + MediaType contentType = response.body().contentType(); + Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8; + if (response.code() < HttpURLConnection.HTTP_MULT_CHOICE || response.code() >= HttpURLConnection.HTTP_BAD_REQUEST) { + is = new InputStreamWithInjectedJS(is, webView.injectedJS, charset, webView.getContext()); + } + return new WebResourceResponse("text/html", charset.name(), is); + } catch (IOException e) { + return null; + } + } + protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) { return new RNCWebView(reactContext); } @@ -162,6 +234,7 @@ protected WebView createViewInstance(ThemedReactContext reactContext) { reactContext.addLifecycleEventListener(webView); mWebViewConfig.configWebView(webView); WebSettings settings = webView.getSettings(); + String USER_AGENT = settings.getUserAgentString(); settings.setBuiltInZoomControls(true); settings.setDisplayZoomControls(false); settings.setDomStorageEnabled(true); @@ -179,10 +252,25 @@ protected WebView createViewInstance(ThemedReactContext reactContext) { new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { WebView.setWebContentsDebuggingEnabled(true); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + ServiceWorkerController swController = ServiceWorkerController.getInstance(); + swController.setServiceWorkerClient(new ServiceWorkerClient() { + @Override + public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { + WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, false, webView); + if (response != null) { + return response; + } + return super.shouldInterceptRequest(request); + } + }); + } + + webView.setDownloadListener(new DownloadListener() { public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { RNCWebViewModule module = getModule(reactContext); @@ -653,7 +741,7 @@ public void onHideCustomView() { } } - protected static class RNCWebViewClient extends WebViewClient { + protected class RNCWebViewClient extends WebViewClient { protected boolean mLastLoadFailed = false; protected @Nullable @@ -699,6 +787,14 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { final String url = request.getUrl().toString(); + // Disabling the URL schemes that cause problems + String[] blacklistedUrls = { "intent:#Intent;action=com.ledger.android.u2f.bridge.AUTHENTICATE" }; + for(int i=0; i< blacklistedUrls.length; i++){ + String badUrl = blacklistedUrls[i]; + if(url.contains(badUrl)){ + return true; + } + } return this.shouldOverrideUrlLoading(view, url); } @@ -745,6 +841,23 @@ protected WritableMap createWebViewEvent(WebView webView, String url) { return event; } + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + return null; + } + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + WebResourceResponse response = null; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + response = RNCWebViewManager.this.shouldInterceptRequest(request, true, (RNCWebView) view); + if (response != null) { + return response; + } + } + + return super.shouldInterceptRequest(view, request); + } + public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) { mUrlPrefixesForDefaultIntent = specialUrls; }