mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Clean up interceptors a bit
This commit is contained in:
		| @@ -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() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user