From a35f94789257cdbfdbe1603df3314b8684ebf7b0 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sun, 25 Sep 2022 23:09:40 +0600 Subject: [PATCH] Cleanup webview interceptors (#8067) * Cleanup webview interceptors * Review changes + Improvement * Review Changes 2 --- .../interceptor/CloudflareInterceptor.kt | 26 ++++++---------- .../network/interceptor/Http103Interceptor.kt | 15 +++++---- .../network/interceptor/WebViewInterceptor.kt | 31 ++++++++++++++++--- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index b467beac0f..3490bf1e46 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.core.R import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.isOutdated -import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.toast import okhttp3.Cookie import okhttp3.HttpUrl.Companion.toHttpUrl @@ -57,25 +56,19 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c // OkHttp doesn't support asynchronous interceptors. val latch = CountDownLatch(1) - var webView: WebView? = null + var webview: WebView? = null var challengeFound = false var cloudflareBypassed = false var isWebViewOutdated = false val origRequestUrl = originalRequest.url.toString() - val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() + val headers = parseHeaders(originalRequest.headers) executor.execute { - val webview = WebView(context) - webView = webview - webview.setDefaultSettings() + webview = createWebView(originalRequest) - // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty - webview.settings.userAgentString = originalRequest.header("User-Agent") - ?: networkHelper.defaultUserAgent - - webview.webViewClient = object : WebViewClientCompat() { + webview?.webViewClient = object : WebViewClientCompat() { override fun onPageFinished(view: WebView, url: String) { fun isCloudFlareBypassed(): Boolean { return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) @@ -113,7 +106,7 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c } } - webView?.loadUrl(origRequestUrl, headers) + webview?.loadUrl(origRequestUrl, headers) } // Wait a reasonable amount of time to retrieve the solution. The minimum should be @@ -122,12 +115,13 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c executor.execute { if (!cloudflareBypassed) { - isWebViewOutdated = webView?.isOutdated() == true + isWebViewOutdated = webview?.isOutdated() == true } - webView?.stopLoading() - webView?.destroy() - webView = null + webview?.run { + stopLoading() + destroy() + } } // Throw exception if we failed to bypass Cloudflare diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/Http103Interceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/Http103Interceptor.kt index 8a7e664919..98f4a6b51f 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/Http103Interceptor.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/Http103Interceptor.kt @@ -43,18 +43,18 @@ class Http103Interceptor(context: Context) : WebViewInterceptor(context) { val jsInterface = JsInterface(latch) - var outerWebView: WebView? = null + var webview: WebView? = null var exception: Exception? = null val requestUrl = originalRequest.url.toString() - val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() + val headers = parseHeaders(originalRequest.headers) executor.execute { - val webview = createWebView(originalRequest).also { outerWebView = it } - webview.addJavascriptInterface(jsInterface, "android") + webview = createWebView(originalRequest) + webview?.addJavascriptInterface(jsInterface, "android") - webview.webViewClient = object : WebViewClientCompat() { + webview?.webViewClient = object : WebViewClientCompat() { override fun onPageFinished(view: WebView, url: String) { view.evaluateJavascript(jsScript) {} } @@ -73,17 +73,16 @@ class Http103Interceptor(context: Context) : WebViewInterceptor(context) { } } - webview.loadUrl(requestUrl, headers) + webview?.loadUrl(requestUrl, headers) } latch.await(10, TimeUnit.SECONDS) executor.execute { - outerWebView?.run { + webview?.run { stopLoading() destroy() } - outerWebView = null } exception?.let { throw it } diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index 13e6bdb519..5247e9f71d 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -12,10 +12,12 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.toast +import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response import uy.kohesive.injekt.injectLazy +import java.util.Locale abstract class WebViewInterceptor(private val context: Context) : Interceptor { @@ -59,10 +61,31 @@ abstract class WebViewInterceptor(private val context: Context) : Interceptor { return intercept(chain, request, response) } + fun parseHeaders(headers: Headers): Map { + return headers + // Keeping unsafe header makes webview throw [net::ERR_INVALID_ARGUMENT] + .filter { (name, value) -> + isRequestHeaderSafe(name, value) + } + .groupBy(keySelector = { (name, _) -> name }) { (_, value) -> value } + .mapValues { it.value.getOrNull(0).orEmpty() } + } + fun createWebView(request: Request): WebView { - val webview = WebView(context) - webview.setDefaultSettings() - webview.settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent - return webview + return WebView(context).apply { + setDefaultSettings() + // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty + settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent + } } } + +// Based on [IsRequestHeaderSafe] in https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc +private fun isRequestHeaderSafe(_name: String, _value: String): Boolean { + val name = _name.lowercase(Locale.ENGLISH) + val value = _value.lowercase(Locale.ENGLISH) + if (name in unsafeHeaderNames || name.startsWith("proxy-")) return false + if (name == "connection" && value == "upgrade") return false + return true +} +private val unsafeHeaderNames = listOf("content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie")