Clean up interceptors a bit

This commit is contained in:
arkon 2022-08-31 15:39:59 -04:00
parent fddca15182
commit dc62d0ea8b
5 changed files with 116 additions and 127 deletions

View File

@ -17,12 +17,13 @@ class NetworkHelper(context: Context) {
private val preferences: PreferencesHelper by injectLazy()
private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieManager = AndroidCookieJar()
private val http103Interceptor = Http103Interceptor(context)
private val userAgentInterceptor by lazy { UserAgentInterceptor() }
private val http103Interceptor by lazy { Http103Interceptor(context) }
private val cloudflareInterceptor by lazy { CloudflareInterceptor(context) }
private val baseClientBuilder: OkHttpClient.Builder
get() {
@ -32,7 +33,7 @@ class NetworkHelper(context: Context) {
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES)
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
.addInterceptor(UserAgentInterceptor())
.addInterceptor(userAgentInterceptor)
.addNetworkInterceptor(http103Interceptor)
if (preferences.verboseLogging()) {
@ -64,7 +65,7 @@ class NetworkHelper(context: Context) {
@Suppress("UNUSED")
val cloudflareClient by lazy {
client.newBuilder()
.addInterceptor(CloudflareInterceptor(context))
.addInterceptor(cloudflareInterceptor)
.build()
}

View File

@ -2,17 +2,12 @@ package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
@ -26,56 +21,26 @@ import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CloudflareInterceptor(private val context: Context) : Interceptor {
class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(context) {
private val executor = ContextCompat.getMainExecutor(context)
private val networkHelper: NetworkHelper by injectLazy()
/**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
* blocking the main thread too much. If used too often we could consider moving it to the
* Application class.
*/
private val initWebView by lazy {
// Crashes on some devices. We skip this in some cases since the only impact is slower
// WebView init in those rare cases.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
return@lazy
}
WebSettings.getDefaultUserAgent(context)
override fun shouldIntercept(response: Response): Boolean {
// Check if Cloudflare anti-bot is on
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
}
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
}
return chain.proceed(originalRequest)
}
initWebView
val response = chain.proceed(originalRequest)
// Check if Cloudflare anti-bot is on
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
return response
}
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
try {
response.close()
networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
networkHelper.cookieManager.remove(request.url, COOKIE_NAMES, 0)
val oldCookie = networkHelper.cookieManager.get(request.url)
.firstOrNull { it.name == "cf_clearance" }
resolveWithWebView(originalRequest, oldCookie)
resolveWithWebView(request, oldCookie)
return chain.proceed(originalRequest)
return chain.proceed(request)
}
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app
@ -87,7 +52,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
private fun resolveWithWebView(originalRequest: Request, oldCookie: Cookie?) {
// We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
@ -98,8 +63,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
var cloudflareBypassed = false
var isWebViewOutdated = false
val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
val origRequestUrl = originalRequest.url.toString()
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
executor.execute {
val webview = WebView(context)
@ -107,7 +72,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
webview.setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent")
webview.settings.userAgentString = originalRequest.header("User-Agent")
?: networkHelper.defaultUserAgent
webview.webViewClient = object : WebViewClientCompat() {
@ -175,12 +140,10 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
throw CloudflareBypassException()
}
}
companion object {
private val ERROR_CODES = listOf(403, 503)
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance")
}
}
private val ERROR_CODES = listOf(403, 503)
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance")
private class CloudflareBypassException : Exception()

View File

@ -2,67 +2,31 @@ package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
// TODO: Remove when OkHttp can handle http 103 responses
class Http103Interceptor(private val context: Context) : Interceptor {
// TODO: Remove when OkHttp can handle HTTP 103 responses
class Http103Interceptor(context: Context) : WebViewInterceptor(context) {
private val executor = ContextCompat.getMainExecutor(context)
private val networkHelper: NetworkHelper by injectLazy()
/**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
* blocking the main thread too much. If used too often we could consider moving it to the
* Application class.
*/
private val initWebView by lazy {
// Crashes on some devices. We skip this in some cases since the only impact is slower
// WebView init in those rare cases.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
return@lazy
}
WebSettings.getDefaultUserAgent(context)
override fun shouldIntercept(response: Response): Boolean {
return response.code == 103
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (response.code != 103) return response
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
}
return response
}
initWebView
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
logcat { "Proceeding with WebView for request $request" }
try {
return proceedWithWebView(request, response)
@ -71,23 +35,9 @@ class Http103Interceptor(private val context: Context) : Interceptor {
}
}
internal class JsInterface(private val latch: CountDownLatch, var payload: String? = null) {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
companion object {
const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
val htmlMediaType = "text/html".toMediaType()
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun proceedWithWebView(ogRequest: Request, ogResponse: Response): Response {
// We need to lock this thread until the WebView finds the challenge solution url, because
private fun proceedWithWebView(originalRequest: Request, originalResponse: Response): Response {
// We need to lock this thread until the WebView loads the page, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
@ -97,16 +47,11 @@ class Http103Interceptor(private val context: Context) : Interceptor {
var exception: Exception? = null
val requestUrl = ogRequest.url.toString()
val headers = ogRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
val requestUrl = originalRequest.url.toString()
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
executor.execute {
val webview = WebView(context).also { outerWebView = it }
with(webview.settings) {
javaScriptEnabled = true
userAgentString = ogRequest.header("User-Agent") ?: networkHelper.defaultUserAgent
}
val webview = createWebView(originalRequest).also { outerWebView = it }
webview.addJavascriptInterface(jsInterface, "android")
webview.webViewClient = object : WebViewClientCompat() {
@ -143,13 +88,25 @@ class Http103Interceptor(private val context: Context) : Interceptor {
exception?.let { throw it }
val payload = jsInterface.payload ?: throw Exception("Couldn't fetch site through webview")
val responseHtml = jsInterface.responseHtml ?: throw Exception("Couldn't fetch site through webview")
return ogResponse.newBuilder()
return originalResponse.newBuilder()
.code(200)
.protocol(Protocol.HTTP_1_1)
.message("OK")
.body(payload.toResponseBody(htmlMediaType))
.body(responseHtml.toResponseBody(htmlMediaType))
.build()
}
}
internal class JsInterface(private val latch: CountDownLatch, var responseHtml: String? = null) {
@Suppress("UNUSED")
@JavascriptInterface
fun passPayload(passedPayload: String) {
responseHtml = passedPayload
latch.countDown()
}
}
private const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
private val htmlMediaType = "text/html".toMediaType()

View File

@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.network.interceptor
import android.content.Context
import android.os.Build
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.lang.launchUI
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.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
abstract class WebViewInterceptor(private val context: Context) : Interceptor {
private val networkHelper: NetworkHelper by injectLazy()
/**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
* blocking the main thread too much. If used too often we could consider moving it to the
* Application class.
*/
private val initWebView by lazy {
// Crashes on some devices. We skip this in some cases since the only impact is slower
// WebView init in those rare cases.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
return@lazy
}
WebSettings.getDefaultUserAgent(context)
}
abstract fun shouldIntercept(response: Response): Boolean
abstract fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (!shouldIntercept(response)) {
return response
}
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
}
return response
}
initWebView
return intercept(chain, request, response)
}
fun createWebView(request: Request): WebView {
val webview = WebView(context)
webview.setDefaultSettings()
webview.settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent
return webview
}
}

View File

@ -11,7 +11,7 @@ import logcat.LogPriority
object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
const val MINIMUM_WEBVIEW_VERSION = 99
const val MINIMUM_WEBVIEW_VERSION = 100
fun supportsWebView(context: Context): Boolean {
try {