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

View file

@ -2,17 +2,12 @@ package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper 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.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
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.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -26,56 +21,26 @@ import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit 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 executor = ContextCompat.getMainExecutor(context)
private val networkHelper: NetworkHelper by injectLazy() private val networkHelper: NetworkHelper by injectLazy()
/** override fun shouldIntercept(response: Response): Boolean {
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid // Check if Cloudflare anti-bot is on
* blocking the main thread too much. If used too often we could consider moving it to the return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
* 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)
} }
@Synchronized override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
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
}
try { try {
response.close() response.close()
networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0) networkHelper.cookieManager.remove(request.url, COOKIE_NAMES, 0)
val oldCookie = networkHelper.cookieManager.get(originalRequest.url) val oldCookie = networkHelper.cookieManager.get(request.url)
.firstOrNull { it.name == "cf_clearance" } .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 // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app // we don't crash the entire app
@ -87,7 +52,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
@SuppressLint("SetJavaScriptEnabled") @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 // We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors. // OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
@ -98,8 +63,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
var cloudflareBypassed = false var cloudflareBypassed = false
var isWebViewOutdated = false var isWebViewOutdated = false
val origRequestUrl = request.url.toString() val origRequestUrl = originalRequest.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
executor.execute { executor.execute {
val webview = WebView(context) val webview = WebView(context)
@ -107,7 +72,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
webview.setDefaultSettings() webview.setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty // 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 ?: networkHelper.defaultUserAgent
webview.webViewClient = object : WebViewClientCompat() { webview.webViewClient = object : WebViewClientCompat() {
@ -175,12 +140,10 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
throw CloudflareBypassException() 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() private class CloudflareBypassException : Exception()

View file

@ -2,67 +2,31 @@ package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat 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.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
// TODO: Remove when OkHttp can handle http 103 responses // TODO: Remove when OkHttp can handle HTTP 103 responses
class Http103Interceptor(private val context: Context) : Interceptor { class Http103Interceptor(context: Context) : WebViewInterceptor(context) {
private val executor = ContextCompat.getMainExecutor(context) private val executor = ContextCompat.getMainExecutor(context)
private val networkHelper: NetworkHelper by injectLazy() override fun shouldIntercept(response: Response): Boolean {
return response.code == 103
/**
* 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 intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): 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
logcat { "Proceeding with WebView for request $request" } logcat { "Proceeding with WebView for request $request" }
try { try {
return proceedWithWebView(request, response) 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") @SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun proceedWithWebView(ogRequest: Request, ogResponse: Response): Response { private fun proceedWithWebView(originalRequest: Request, originalResponse: Response): Response {
// We need to lock this thread until the WebView finds the challenge solution url, because // We need to lock this thread until the WebView loads the page, because
// OkHttp doesn't support asynchronous interceptors. // OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
@ -97,16 +47,11 @@ class Http103Interceptor(private val context: Context) : Interceptor {
var exception: Exception? = null var exception: Exception? = null
val requestUrl = ogRequest.url.toString() val requestUrl = originalRequest.url.toString()
val headers = ogRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
executor.execute { executor.execute {
val webview = WebView(context).also { outerWebView = it } val webview = createWebView(originalRequest).also { outerWebView = it }
with(webview.settings) {
javaScriptEnabled = true
userAgentString = ogRequest.header("User-Agent") ?: networkHelper.defaultUserAgent
}
webview.addJavascriptInterface(jsInterface, "android") webview.addJavascriptInterface(jsInterface, "android")
webview.webViewClient = object : WebViewClientCompat() { webview.webViewClient = object : WebViewClientCompat() {
@ -143,13 +88,25 @@ class Http103Interceptor(private val context: Context) : Interceptor {
exception?.let { throw it } 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) .code(200)
.protocol(Protocol.HTTP_1_1) .protocol(Protocol.HTTP_1_1)
.message("OK") .message("OK")
.body(payload.toResponseBody(htmlMediaType)) .body(responseHtml.toResponseBody(htmlMediaType))
.build() .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 { object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome" 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 { fun supportsWebView(context: Context): Boolean {
try { try {