Просмотр исходного кода

Cleanup webview interceptors (#8067)

* Cleanup webview interceptors

* Review changes + Improvement

* Review Changes 2
AntsyLich 2 лет назад
Родитель
Сommit
a35f947892

+ 10 - 16
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

+ 7 - 8
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 }

+ 27 - 4
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<String, String> {
+        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")