Add an UserAgent Interceptor to Cloudflare Client

This commit is contained in:
Thiago França da Silva 2020-03-09 15:10:10 -03:00 committed by Jay
parent 329e8f1988
commit 4e54690229
4 changed files with 44 additions and 31 deletions

View File

@ -8,6 +8,7 @@ import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource
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.toast import eu.kanade.tachiyomi.util.system.toast
@ -55,18 +56,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
resolveWithWebView(originalRequest, oldCookie) resolveWithWebView(originalRequest, oldCookie)
// Avoid use empty User-Agent return chain.proceed(originalRequest)
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent",
DEFAULT_USERAGENT)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
} catch (e: Exception) { } catch (e: Exception) {
// 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
@ -84,11 +74,10 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
var challengeFound = false var challengeFound = false
var cloudflareBypassed = false var cloudflareBypassed = false
var isWebviewOutdated = false var isWebViewOutdated = false
val origRequestUrl = request.url.toString() val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
val withUserAgent = request.header("User-Agent").isNullOrEmpty()
handler.post { handler.post {
val webview = WebView(context) val webview = WebView(context)
@ -96,15 +85,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
webview.settings.javaScriptEnabled = true webview.settings.javaScriptEnabled = true
// Avoid set empty User-Agent, Chromium WebView will reset to default if empty // Avoid set empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent") webview.settings.userAgentString =
?: DEFAULT_USERAGENT request.header("User-Agent") ?: HttpSource.DEFAULT_USERAGENT
webview.webViewClient = object : WebViewClientCompat() { 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())
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
.let { it != null && (it != oldCookie || withUserAgent) } .let { it != null && it != oldCookie }
} }
if (isCloudFlareBypassed()) { if (isCloudFlareBypassed()) {
@ -147,7 +136,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
handler.post { handler.post {
if (!cloudflareBypassed) { if (!cloudflareBypassed) {
isWebviewOutdated = webView?.isOutdated() == true isWebViewOutdated = webView?.isOutdated() == true
} }
webView?.stopLoading() webView?.stopLoading()
@ -157,7 +146,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
// Throw exception if we failed to bypass Cloudflare // Throw exception if we failed to bypass Cloudflare
if (!cloudflareBypassed) { if (!cloudflareBypassed) {
// Prompt user to update WebView if it seems too outdated // Prompt user to update WebView if it seems too outdated
if (isWebviewOutdated) { if (isWebViewOutdated) {
context.toast(R.string.please_update_webview, Toast.LENGTH_LONG) context.toast(R.string.please_update_webview, Toast.LENGTH_LONG)
} }
@ -168,6 +157,5 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
companion object { companion object {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance") private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
private const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
} }
} }

View File

@ -19,6 +19,7 @@ class NetworkHelper(context: Context) {
.build() .build()
val cloudflareClient = client.newBuilder() val cloudflareClient = client.newBuilder()
.addInterceptor(UserAgentInterceptor())
.addInterceptor(CloudflareInterceptor(context)) .addInterceptor(CloudflareInterceptor(context))
.build() .build()
} }

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.network
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Interceptor
import okhttp3.Response
class UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
}
}

View File

@ -74,7 +74,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
protected open fun headersBuilder() = Headers.Builder().apply { protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") add("User-Agent", DEFAULT_USERAGENT)
} }
/** /**
@ -207,14 +207,14 @@ abstract class HttpSource : CatalogueSource {
* @param manga the manga to look for chapters. * @param manga the manga to look for chapters.
*/ */
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
if (manga.status != SManga.LICENSED) { return if (manga.status != SManga.LICENSED) {
return client.newCall(chapterListRequest(manga)) client.newCall(chapterListRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
chapterListParse(response) chapterListParse(response)
} }
} else { } else {
return Observable.error(Exception("Licensed - No chapters to show")) Observable.error(Exception("Licensed - No chapters to show"))
} }
} }
@ -340,16 +340,14 @@ abstract class HttpSource : CatalogueSource {
* @param orig the full url. * @param orig the full url.
*/ */
private fun getUrlWithoutDomain(orig: String): String { private fun getUrlWithoutDomain(orig: String): String {
try { return try {
val uri = URI(orig) val uri = URI(orig)
var out = uri.path var out = uri.path
if (uri.query != null) if (uri.query != null) out += "?" + uri.query
out += "?" + uri.query if (uri.fragment != null) out += "#" + uri.fragment
if (uri.fragment != null) out
out += "#" + uri.fragment
return out
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
return orig orig
} }
} }
@ -367,4 +365,8 @@ abstract class HttpSource : CatalogueSource {
* Returns the list of filters for the source. * Returns the list of filters for the source.
*/ */
override fun getFilterList() = FilterList() override fun getFilterList() = FilterList()
companion object {
const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
}
} }