Cleanup webview interceptors (#8067)

* Cleanup webview interceptors

* Review changes + Improvement

* Review Changes 2
This commit is contained in:
AntsyLich 2022-09-25 23:09:40 +06:00 committed by GitHub
parent ec272f6c4e
commit a35f947892
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 44 additions and 28 deletions

View File

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -57,25 +56,19 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c
// OkHttp doesn't support asynchronous interceptors. // OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
var webView: WebView? = null var webview: WebView? = null
var challengeFound = false var challengeFound = false
var cloudflareBypassed = false var cloudflareBypassed = false
var isWebViewOutdated = false var isWebViewOutdated = false
val origRequestUrl = originalRequest.url.toString() val origRequestUrl = originalRequest.url.toString()
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() val headers = parseHeaders(originalRequest.headers)
executor.execute { executor.execute {
val webview = WebView(context) webview = createWebView(originalRequest)
webView = webview
webview.setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty webview?.webViewClient = object : WebViewClientCompat() {
webview.settings.userAgentString = originalRequest.header("User-Agent")
?: networkHelper.defaultUserAgent
webview.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
fun isCloudFlareBypassed(): Boolean { fun isCloudFlareBypassed(): Boolean {
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) 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 // 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 { executor.execute {
if (!cloudflareBypassed) { if (!cloudflareBypassed) {
isWebViewOutdated = webView?.isOutdated() == true isWebViewOutdated = webview?.isOutdated() == true
} }
webView?.stopLoading() webview?.run {
webView?.destroy() stopLoading()
webView = null destroy()
}
} }
// Throw exception if we failed to bypass Cloudflare // Throw exception if we failed to bypass Cloudflare

View File

@ -43,18 +43,18 @@ class Http103Interceptor(context: Context) : WebViewInterceptor(context) {
val jsInterface = JsInterface(latch) val jsInterface = JsInterface(latch)
var outerWebView: WebView? = null var webview: WebView? = null
var exception: Exception? = null var exception: Exception? = null
val requestUrl = originalRequest.url.toString() val requestUrl = originalRequest.url.toString()
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() val headers = parseHeaders(originalRequest.headers)
executor.execute { executor.execute {
val webview = createWebView(originalRequest).also { outerWebView = it } webview = createWebView(originalRequest)
webview.addJavascriptInterface(jsInterface, "android") webview?.addJavascriptInterface(jsInterface, "android")
webview.webViewClient = object : WebViewClientCompat() { webview?.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
view.evaluateJavascript(jsScript) {} 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) latch.await(10, TimeUnit.SECONDS)
executor.execute { executor.execute {
outerWebView?.run { webview?.run {
stopLoading() stopLoading()
destroy() destroy()
} }
outerWebView = null
} }
exception?.let { throw it } exception?.let { throw it }

View File

@ -12,10 +12,12 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Locale
abstract class WebViewInterceptor(private val context: Context) : Interceptor { 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) 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 { fun createWebView(request: Request): WebView {
val webview = WebView(context) return WebView(context).apply {
webview.setDefaultSettings() setDefaultSettings()
webview.settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
return webview 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")