Cleanup webview interceptors (#8067)
* Cleanup webview interceptors * Review changes + Improvement * Review Changes 2
This commit is contained in:
parent
ec272f6c4e
commit
a35f947892
@ -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
|
||||||
|
@ -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 }
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user