mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05:00
Clean up interceptors a bit
This commit is contained in:
parent
fddca15182
commit
dc62d0ea8b
5 changed files with 116 additions and 127 deletions
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue