From d72d9ca2cb8d6d8efcb7dd2824d551033eb1dece Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Mon, 18 Mar 2024 23:24:07 +1100 Subject: [PATCH 01/11] feat: added FlareSolverr to bypass CF anti-bot. Signed-off-by: KaiserBh --- .../settings/screen/SettingsAdvancedScreen.kt | 97 ++++++++++ .../kanade/tachiyomi/network/NetworkHelper.kt | 4 +- .../tachiyomi/network/NetworkPreferences.kt | 8 + .../interceptor/CloudflareInterceptor.kt | 10 +- .../interceptor/FlareSolverrInterceptor.kt | 166 ++++++++++++++++++ .../commonMain/resources/MR/base/strings.xml | 7 +- 6 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index e17f06a550fb..7311f1d0e451 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -5,6 +5,8 @@ import android.content.ActivityNotFoundException import android.content.Intent import android.os.Build import android.provider.Settings +import android.util.Log +import tachiyomi.core.common.preference.Preference as BasePreference import android.webkit.WebStorage import android.webkit.WebView import android.widget.Toast @@ -54,6 +56,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101 import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9 import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN +import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import eu.kanade.tachiyomi.source.AndroidSourceManager import eu.kanade.tachiyomi.ui.more.OnboardingScreen import eu.kanade.tachiyomi.util.CrashLogUtil @@ -74,10 +77,17 @@ import exh.util.toAnnotatedString import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableMap +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import logcat.LogPriority import okhttp3.Headers +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject import tachiyomi.core.common.i18n.pluralStringResource import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchNonCancellable @@ -249,9 +259,13 @@ object SettingsAdvancedScreen : SearchableSettings { ): Preference.PreferenceGroup { val context = LocalContext.current val networkHelper = remember { Injekt.get() } + val scope = rememberCoroutineScope() val userAgentPref = networkPreferences.defaultUserAgent() val userAgent by userAgentPref.collectAsState() + val flareSolverrUrlPref = networkPreferences.flareSolverrUrl() + val enableFlareSolverrPref = networkPreferences.enableFlareSolverr() + val enableFlareSolverr by enableFlareSolverrPref.collectAsState() return Preference.PreferenceGroup( title = stringResource(MR.strings.label_network), @@ -330,6 +344,27 @@ object SettingsAdvancedScreen : SearchableSettings { context.toast(MR.strings.requires_app_restart) }, ), + Preference.PreferenceItem.SwitchPreference( + pref = enableFlareSolverrPref, + title = stringResource(MR.strings.pref_enable_flare_solverr), + subtitle = stringResource(MR.strings.pref_enable_flare_solverr_summary) + ), + Preference.PreferenceItem.EditTextPreference( + pref = flareSolverrUrlPref, + title = stringResource(MR.strings.pref_flare_solverr_url), + enabled = enableFlareSolverr, + subtitle = stringResource(MR.strings.pref_flare_solverr_url_summary), + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_test_flare_solverr_and_update_user_agent), + enabled = enableFlareSolverr, + onClick = { + scope.launch { + testFlareSolverrAndUpdateUserAgent(flareSolverrUrlPref, userAgentPref, context) + } + }, + ) + ), ) } @@ -764,6 +799,68 @@ object SettingsAdvancedScreen : SearchableSettings { ) } + private suspend fun testFlareSolverrAndUpdateUserAgent( + flareSolverrUrlPref: BasePreference, + userAgentPref: BasePreference, + context: android.content.Context + ) { + try { + withContext(Dispatchers.IO) { + val client = OkHttpClient() + val flareSolverUrl = flareSolverrUrlPref.get().trim() + val mediaType = "application/json; charset=utf-8".toMediaType() + val data = JSONObject() + .put("cmd", "request.get") + .put("url", "https://www.google.com/") + .put("maxTimeout", 60000) + .put("returnOnlyCookies", true) + .toString() + val body = data.toRequestBody(mediaType) + val request = Request.Builder() + .url(flareSolverUrl) + .post(body) + .header("Content-Type", "application/json") + .build() + + Log.d("FlareSolverrRequest", "Sending request to FlareSolverr: $flareSolverUrl with payload: $data") + + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + Log.e("HttpError", "Request failed with status code: ${response.code}") + throw CloudflareBypassException("Failed with status code: ${response.code}") + } + + val responseBody = response.body.string() + Log.d("HttpResponse", responseBody) + + val jsonResponse = JSONObject(responseBody) + val status = jsonResponse.optString("status") + if (status == "ok") { + val newUserAgent = jsonResponse.optJSONObject("solution")?.getString("userAgent") + newUserAgent?.let { + userAgentPref.set(it) + Log.d("FlareSolverrInterceptor", "User agent updated to: $it") + } + val message = "FlareSolverr is working. User agent updated. Please restart the app" + withContext(Dispatchers.Main) { + context.toast(message) + } + } else { + val message = "FlareSolverr is not working." + withContext(Dispatchers.Main) { + context.toast(message) + } + } + } + } catch (e: Exception) { + Log.e("FlareSolverrInterceptor", "Error: ${e.message}", e) + withContext(Dispatchers.Main) { + context.toast("Error contacting FlareSolverr") + } + } + } + + private var job: Job? = null // SY <-- } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index f508e6e08b25..8948b721250e 100755 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network import android.content.Context import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor +import eu.kanade.tachiyomi.network.interceptor.FlareSolverrInterceptor import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor @@ -41,6 +42,7 @@ open /* SY <-- */ class NetworkHelper( .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) .addNetworkInterceptor(IgnoreGzipInterceptor()) .addNetworkInterceptor(BrotliInterceptor) + .addNetworkInterceptor(FlareSolverrInterceptor(preferences)) if (isDebugBuild) { val httpLoggingInterceptor = HttpLoggingInterceptor().apply { @@ -50,7 +52,7 @@ open /* SY <-- */ class NetworkHelper( } builder.addInterceptor( - CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider), + CloudflareInterceptor(context, cookieJar, preferences, ::defaultUserAgentProvider), ) when (preferences.dohProvider().get()) { diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt index c32864aec60f..63bc29055c67 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -12,6 +12,14 @@ class NetworkPreferences( return preferenceStore.getBoolean("verbose_logging", verboseLogging) } + fun enableFlareSolverr(): Preference { + return preferenceStore.getBoolean("enable_flare_solverr", false) + } + + fun flareSolverrUrl(): Preference { + return preferenceStore.getString("flare_solverr_url", "http://localhost:8191/v1") + } + fun dohProvider(): Preference { return preferenceStore.getInt("doh_provider", -1) } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 6a765c680ee8..4ea59b8cd268 100755 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -6,6 +6,7 @@ import android.webkit.WebView import android.widget.Toast import androidx.core.content.ContextCompat import eu.kanade.tachiyomi.network.AndroidCookieJar +import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.toast @@ -22,6 +23,7 @@ import java.util.concurrent.CountDownLatch class CloudflareInterceptor( private val context: Context, private val cookieManager: AndroidCookieJar, + private val preferences: NetworkPreferences, defaultUserAgentProvider: () -> String, ) : WebViewInterceptor(context, defaultUserAgentProvider) { @@ -38,6 +40,10 @@ class CloudflareInterceptor( response: Response, ): Response { try { + // If FlareSolverr is enabled, just proceed with the request, no need to try and bypass CF with webview. + if (preferences.enableFlareSolverr().get()){ + return chain.proceed(request) + } response.close() cookieManager.remove(request.url, COOKIE_NAMES, 0) val oldCookie = cookieManager.get(request.url) @@ -134,7 +140,7 @@ class CloudflareInterceptor( context.toast(MR.strings.information_webview_outdated, Toast.LENGTH_LONG) } - throw CloudflareBypassException() + throw CloudflareBypassException("Error resolving with WebView") } } } @@ -143,4 +149,4 @@ 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() +class CloudflareBypassException(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt new file mode 100644 index 000000000000..87a45169ff1a --- /dev/null +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -0,0 +1,166 @@ +package eu.kanade.tachiyomi.network.interceptor + +import android.net.Uri +import android.util.Log +import android.webkit.CookieManager +import eu.kanade.tachiyomi.network.NetworkPreferences +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.json.JSONObject + +class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Interceptor { + private val cookieManager = CookieManager.getInstance() + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + // FlareSolverr is disabled, so just proceed with the request. + if (!preferences.enableFlareSolverr().get()){ + return chain.proceed(request) + } + + Log.d("FlareSolverrInterceptor", "Intercepting request: ${request.url}") + + // First, ensure cf_clearance for subdomains if needed. + ensureCfClearanceForSubdomain(request.url.toString()) + + // Then, check if the cf_clearance cookie exists and is valid for the base domain. + if (!isCfClearanceCookieValid(request.url.toString())) { + // If the cf_clearance cookie is missing or not valid, get the cookies string from FlareSolverr. + val cookiesString = resolveWithFlareSolverr(request) + + // If cookies were found, parse and add them to the cookie jar. + cookiesString.takeIf { it.isNotBlank() }?.let { cookies -> + val url = request.url.toString() + cookies.split("; ").forEach { cookie -> + Log.d("FlareSolverrInterceptor", "Adding cookie: $cookie, to $url") + cookieManager.setCookie(url, cookie) + } + } + } + + return chain.proceed(request) + } + + private fun resolveWithFlareSolverr(originalRequest: Request): String { + try { + val client = OkHttpClient() + + val flareSolverUrl = preferences.flareSolverrUrl().get().trim() + val mediaType = "application/json; charset=utf-8".toMediaType() + val data = JSONObject() + .put("cmd", "request.get") + .put("url", originalRequest.url.toString()) + .put("maxTimeout", 60000) + .put("returnOnlyCookies", true) + .toString() + val body = data.toRequestBody(mediaType) + val request = Request.Builder() + .url(flareSolverUrl) + .post(body) + .header("Content-Type", "application/json") + .build() + + Log.d("FlareSolverrRequest", "Sending request to FlareSolverr: $flareSolverUrl with payload: $data") + + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + Log.e("HttpError", "Request failed with status code: ${response.code}") + throw CloudflareBypassException("Failed with status code: ${response.code}") + } + + val responseBody = response.body.string() + Log.d("HttpResponse", responseBody) + + val jsonResponse = JSONObject(responseBody) + val status = jsonResponse.optString("status") + if (status == "ok") { + val solution = jsonResponse.optJSONObject("solution") + val cookiesArray = solution?.optJSONArray("cookies") + + val cookieStringBuilder = StringBuilder() + cookiesArray?.let { + for (i in 0 until it.length()) { + val cookieObj = it.getJSONObject(i) + val cookieString = "${cookieObj.getString("name")}=${cookieObj.getString("value")};" + cookieStringBuilder.append(cookieString) + if (i < cookiesArray.length() - 1) { + cookieStringBuilder.append(" ") + } + } + } + return cookieStringBuilder.toString().trimEnd() + } else { + throw CloudflareBypassException("Failed to solve challenge: $status") + } + } catch (e: Exception) { + Log.e("HttpError", "Failed to resolve with FlareSolverr: ${e.message}", e) + if (e is CloudflareBypassException) throw e + throw CloudflareBypassException("Error resolving with FlareSolverr", e) + } + } + + /** + * Checks if the `cf_clearance` cookie is present and valid for the base domain of the provided URL. + * This is a critical check when accessing Cloudflare-protected sites, as the presence of this cookie + * suggests that Cloudflare's challenge has already been solved for this domain. The method extends + * this check to cover both the base domain and any subdomains, ensuring comprehensive coverage. + * + * @param url The URL as a [String] for which to verify the presence of the `cf_clearance` cookie. + * @return [Boolean] `true` if the `cf_clearance` cookie is found for the base domain of the URL, + * indicating that Cloudflare's challenge page might not be triggered. Returns `false` if the cookie + * is not found or considered invalid, suggesting that access may be blocked by Cloudflare. + */ + private fun isCfClearanceCookieValid(url: String): Boolean { + // Checks if the cf_clearance cookie is valid. This function has been updated to check for the cookie's presence across both the base domain and any subdomains. + val baseDomain = getBaseDomain(url) + val checkUrl = "https://www.$baseDomain/" + val cookiesStringForBaseDomain = cookieManager.getCookie(checkUrl) + val hasCfClearanceCookie = cookiesStringForBaseDomain?.contains("cf_clearance=") ?: false + Log.d("CookieManager", "cf_clearance cookie for $checkUrl: $hasCfClearanceCookie") + return hasCfClearanceCookie + } + + /** + * Ensures that if a valid `cf_clearance` cookie exists for the base domain, it is also set for + * any subdomains accessed. This method is useful for scenarios where a Cloudflare-protected + * site's base domain has passed Cloudflare's challenge, and subsequent requests to its subdomains + * should inherit this clearance without needing to solve the challenge again. + * + * The method checks for the `cf_clearance` cookie's presence and validity for the base domain. + * If found and valid, it sets the same `cf_clearance` cookie for the subdomain of the URL provided. + * + * @param url The URL as a [String] indicating the subdomain for which the `cf_clearance` cookie + * should be set, assuming it's valid for the base domain. + */ + private fun ensureCfClearanceForSubdomain(url: String) { + val uri = Uri.parse(url) + val host = uri.host ?: return + val baseDomain = getBaseDomain(url) + if (host != "www.$baseDomain" && isCfClearanceCookieValid("https://www.$baseDomain/")) { + val cfClearanceValue = getCookieValueForDomain("https://www.$baseDomain/", "cf_clearance") + if (cfClearanceValue != null) { + val subdomainUrl = "https://$host/" + cookieManager.setCookie(subdomainUrl, "cf_clearance=$cfClearanceValue") + Log.d("CookieManager", "Set cf_clearance for $subdomainUrl: cf_clearance=$cfClearanceValue") + } + } + } + + private fun getBaseDomain(url: String): String { + val uri = Uri.parse(url) + val host = uri.host ?: return "" + val parts = host.split(".") + return if (parts.size >= 2) parts.takeLast(2).joinToString(".") else host + } + + private fun getCookieValueForDomain(url: String, cookieName: String): String? { + val cookiesString = cookieManager.getCookie(url) + return cookiesString?.split("; ")?.firstOrNull { it.startsWith("$cookieName=") } + ?.substringAfter("$cookieName=") + } +} diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index d7798ff45a52..bfd75fc94c66 100755 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -631,8 +631,13 @@ Verbose logging Print verbose logs to system log (reduces app performance) Debug info + Enable FlareSolverr + Use FlareSolverr to bypass Cloudflare\'s anti-bot protection + FlareSolverr URL + URL of the FlareSolverr instance to use for example http://192.168.1.202:8191/v1 or https://api.example.com + Test FlareSolverr and update user agent - + Website Version What\'s new From dd7c874de78df0623ed9d4d0adf44aa2cc92134f Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Mon, 18 Mar 2024 23:31:08 +1100 Subject: [PATCH 02/11] chore: add summary to Test FlareSolverr button. Signed-off-by: KaiserBh --- .../presentation/more/settings/screen/SettingsAdvancedScreen.kt | 1 + i18n/src/commonMain/resources/MR/base/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index 7311f1d0e451..89d30321891b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -358,6 +358,7 @@ object SettingsAdvancedScreen : SearchableSettings { Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.pref_test_flare_solverr_and_update_user_agent), enabled = enableFlareSolverr, + subtitle = stringResource(MR.strings.pref_test_flare_solverr_and_update_user_agent_summary), onClick = { scope.launch { testFlareSolverrAndUpdateUserAgent(flareSolverrUrlPref, userAgentPref, context) diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index bfd75fc94c66..f996738f0592 100755 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -636,6 +636,7 @@ FlareSolverr URL URL of the FlareSolverr instance to use for example http://192.168.1.202:8191/v1 or https://api.example.com Test FlareSolverr and update user agent + Test FlareSolverr to update the user agent. Only required once; then toggle as needed. Website From aab8b6ea87d5b548a0962def051c2359875e241e Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Tue, 19 Mar 2024 00:30:43 +1100 Subject: [PATCH 03/11] refactor: move the check into the ShouldIntercept. Signed-off-by: KaiserBh --- .../network/interceptor/CloudflareInterceptor.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 4ea59b8cd268..a9cac1d4dda1 100755 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -30,6 +30,11 @@ class CloudflareInterceptor( private val executor = ContextCompat.getMainExecutor(context) override fun shouldIntercept(response: Response): Boolean { + // Check if FlareSolverr is enabled if it's enabled we don't need to bypass Cloudflare through WebView + if (preferences.enableFlareSolverr().get()){ + return false + } + // Check if Cloudflare anti-bot is on return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK } @@ -40,10 +45,6 @@ class CloudflareInterceptor( response: Response, ): Response { try { - // If FlareSolverr is enabled, just proceed with the request, no need to try and bypass CF with webview. - if (preferences.enableFlareSolverr().get()){ - return chain.proceed(request) - } response.close() cookieManager.remove(request.url, COOKIE_NAMES, 0) val oldCookie = cookieManager.get(request.url) From 15db7c58aabb3edc27290598e7af5025587a4ec3 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Tue, 19 Mar 2024 00:31:23 +1100 Subject: [PATCH 04/11] refactor: add the option to only add cf_clearance cookie. Signed-off-by: KaiserBh --- .../network/interceptor/FlareSolverrInterceptor.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt index 87a45169ff1a..93515beaa675 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -46,7 +46,7 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int return chain.proceed(request) } - private fun resolveWithFlareSolverr(originalRequest: Request): String { + private fun resolveWithFlareSolverr(originalRequest: Request, addAllCookies: Boolean = true): String { try { val client = OkHttpClient() @@ -86,10 +86,13 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int cookiesArray?.let { for (i in 0 until it.length()) { val cookieObj = it.getJSONObject(i) - val cookieString = "${cookieObj.getString("name")}=${cookieObj.getString("value")};" - cookieStringBuilder.append(cookieString) - if (i < cookiesArray.length() - 1) { - cookieStringBuilder.append(" ") + // Check if we should add all cookies or just cf_clearance + if (addAllCookies || cookieObj.getString("name") == "cf_clearance") { + val cookieString = "${cookieObj.getString("name")}=${cookieObj.getString("value")};" + cookieStringBuilder.append(cookieString) + if (i < cookiesArray.length() - 1) { + cookieStringBuilder.append(" ") + } } } } From 51896bf20982686aec51c5cfe92b955ec73a4a24 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Tue, 19 Mar 2024 00:32:31 +1100 Subject: [PATCH 05/11] refactor: update summary. I don't think reverse proxy going to work maybe it does but no luck on my end not that I need it tbh, I think using tailscale or wireguard should suffice and safer as there is no security in flaresolverr. Signed-off-by: KaiserBh --- i18n/src/commonMain/resources/MR/base/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index f996738f0592..e51d2c4eb8fe 100755 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -634,7 +634,7 @@ Enable FlareSolverr Use FlareSolverr to bypass Cloudflare\'s anti-bot protection FlareSolverr URL - URL of the FlareSolverr instance to use for example http://192.168.1.202:8191/v1 or https://api.example.com + URL of the FlareSolverr instance to use for example http://192.168.1.202:8191/v1 Test FlareSolverr and update user agent Test FlareSolverr to update the user agent. Only required once; then toggle as needed. From 299cdd82166dcca08f53162131dc0e0055f96bec Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Tue, 19 Mar 2024 01:40:53 +1100 Subject: [PATCH 06/11] refactor: Improved cookie handling. We should make sure the cookie is attached when the request proceeds that way we don't have to relay on the app cookie jar state, for example we get the result immediately instead of going out of the extension and going back in, same withe the chapters, they use subdomain and it's random, so we need to make sure the cf_clearance is added to the subdomains dynamically. Signed-off-by: KaiserBh --- .../interceptor/FlareSolverrInterceptor.kt | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt index 93515beaa675..beb68ad0d031 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -16,34 +16,51 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int private val cookieManager = CookieManager.getInstance() override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() + val originalRequest = chain.request() // FlareSolverr is disabled, so just proceed with the request. - if (!preferences.enableFlareSolverr().get()){ - return chain.proceed(request) + if (!preferences.enableFlareSolverr().get()) { + return chain.proceed(originalRequest) } - Log.d("FlareSolverrInterceptor", "Intercepting request: ${request.url}") + Log.d("FlareSolverrInterceptor", "Intercepting request: ${originalRequest.url}") // First, ensure cf_clearance for subdomains if needed. - ensureCfClearanceForSubdomain(request.url.toString()) + ensureCfClearanceForSubdomain(originalRequest.url.toString()) - // Then, check if the cf_clearance cookie exists and is valid for the base domain. - if (!isCfClearanceCookieValid(request.url.toString())) { + var cookiesString = "" + + // Check if the cf_clearance cookie exists and is valid for the base domain or the current subdomain. + if (!isCfClearanceCookieValid(originalRequest.url.toString())) { // If the cf_clearance cookie is missing or not valid, get the cookies string from FlareSolverr. - val cookiesString = resolveWithFlareSolverr(request) + cookiesString = resolveWithFlareSolverr(originalRequest) // If cookies were found, parse and add them to the cookie jar. cookiesString.takeIf { it.isNotBlank() }?.let { cookies -> - val url = request.url.toString() + val url = originalRequest.url.toString() cookies.split("; ").forEach { cookie -> Log.d("FlareSolverrInterceptor", "Adding cookie: $cookie, to $url") cookieManager.setCookie(url, cookie) } } + } else { + // Here, the cf_clearance cookie is valid; we need to ensure it's included in the request for subdomains. + val cfClearanceValue = getCookieValueForDomain(originalRequest.url.toString(), CF_COOKIE_NAME) + if (cfClearanceValue != null) { + cookiesString = "cf_clearance=$cfClearanceValue" + } } - return chain.proceed(request) + // Create a new request with the cookies, whether they are newly obtained or retrieved from the cookie jar. + val newRequest = if (cookiesString.isNotBlank()) { + originalRequest.newBuilder() + .header("Cookie", cookiesString.trimEnd(';')) + .build() + } else { + originalRequest + } + + return chain.proceed(newRequest) } private fun resolveWithFlareSolverr(originalRequest: Request, addAllCookies: Boolean = true): String { @@ -145,7 +162,7 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int val host = uri.host ?: return val baseDomain = getBaseDomain(url) if (host != "www.$baseDomain" && isCfClearanceCookieValid("https://www.$baseDomain/")) { - val cfClearanceValue = getCookieValueForDomain("https://www.$baseDomain/", "cf_clearance") + val cfClearanceValue = getCookieValueForDomain("https://www.$baseDomain/", CF_COOKIE_NAME) if (cfClearanceValue != null) { val subdomainUrl = "https://$host/" cookieManager.setCookie(subdomainUrl, "cf_clearance=$cfClearanceValue") @@ -167,3 +184,5 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int ?.substringAfter("$cookieName=") } } + +private const val CF_COOKIE_NAME = "cf_clearance" From ead9079d730a885d0864ddcae008448ef912ad06 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Tue, 19 Mar 2024 01:51:33 +1100 Subject: [PATCH 07/11] chore: detekt stuff. Signed-off-by: KaiserBh --- .../settings/screen/SettingsAdvancedScreen.kt | 99 +++++++++---------- .../interceptor/CloudflareInterceptor.kt | 2 +- .../interceptor/FlareSolverrInterceptor.kt | 1 - 3 files changed, 50 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index 89d30321891b..1637130cf959 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.os.Build import android.provider.Settings import android.util.Log -import tachiyomi.core.common.preference.Preference as BasePreference import android.webkit.WebStorage import android.webkit.WebView import android.widget.Toast @@ -106,6 +105,7 @@ import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File +import tachiyomi.core.common.preference.Preference as BasePreference object SettingsAdvancedScreen : SearchableSettings { @@ -356,7 +356,7 @@ object SettingsAdvancedScreen : SearchableSettings { subtitle = stringResource(MR.strings.pref_flare_solverr_url_summary), ), Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_test_flare_solverr_and_update_user_agent), + title = stringResource(MR.strings.pref_test_flare_solverr_and_update_user_agent), enabled = enableFlareSolverr, subtitle = stringResource(MR.strings.pref_test_flare_solverr_and_update_user_agent_summary), onClick = { @@ -805,63 +805,62 @@ object SettingsAdvancedScreen : SearchableSettings { userAgentPref: BasePreference, context: android.content.Context ) { - try { - withContext(Dispatchers.IO) { - val client = OkHttpClient() - val flareSolverUrl = flareSolverrUrlPref.get().trim() - val mediaType = "application/json; charset=utf-8".toMediaType() - val data = JSONObject() - .put("cmd", "request.get") - .put("url", "https://www.google.com/") - .put("maxTimeout", 60000) - .put("returnOnlyCookies", true) - .toString() - val body = data.toRequestBody(mediaType) - val request = Request.Builder() - .url(flareSolverUrl) - .post(body) - .header("Content-Type", "application/json") - .build() + try { + withContext(Dispatchers.IO) { + val client = OkHttpClient() + val flareSolverUrl = flareSolverrUrlPref.get().trim() + val mediaType = "application/json; charset=utf-8".toMediaType() + val data = JSONObject() + .put("cmd", "request.get") + .put("url", "https://www.google.com/") + .put("maxTimeout", 60000) + .put("returnOnlyCookies", true) + .toString() + val body = data.toRequestBody(mediaType) + val request = Request.Builder() + .url(flareSolverUrl) + .post(body) + .header("Content-Type", "application/json") + .build() - Log.d("FlareSolverrRequest", "Sending request to FlareSolverr: $flareSolverUrl with payload: $data") + Log.d("FlareSolverrRequest", "Sending request to FlareSolverr: $flareSolverUrl with payload: $data") - val response = client.newCall(request).execute() - if (!response.isSuccessful) { - Log.e("HttpError", "Request failed with status code: ${response.code}") - throw CloudflareBypassException("Failed with status code: ${response.code}") - } + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + Log.e("HttpError", "Request failed with status code: ${response.code}") + throw CloudflareBypassException("Failed with status code: ${response.code}") + } - val responseBody = response.body.string() - Log.d("HttpResponse", responseBody) + val responseBody = response.body.string() + Log.d("HttpResponse", responseBody) - val jsonResponse = JSONObject(responseBody) - val status = jsonResponse.optString("status") - if (status == "ok") { - val newUserAgent = jsonResponse.optJSONObject("solution")?.getString("userAgent") - newUserAgent?.let { - userAgentPref.set(it) - Log.d("FlareSolverrInterceptor", "User agent updated to: $it") - } - val message = "FlareSolverr is working. User agent updated. Please restart the app" - withContext(Dispatchers.Main) { - context.toast(message) - } - } else { - val message = "FlareSolverr is not working." - withContext(Dispatchers.Main) { - context.toast(message) - } + val jsonResponse = JSONObject(responseBody) + val status = jsonResponse.optString("status") + if (status == "ok") { + val newUserAgent = jsonResponse.optJSONObject("solution")?.getString("userAgent") + newUserAgent?.let { + userAgentPref.set(it) + Log.d("FlareSolverrInterceptor", "User agent updated to: $it") + } + val message = "FlareSolverr is working. User agent updated. Please restart the app" + withContext(Dispatchers.Main) { + context.toast(message) + } + } else { + val message = "FlareSolverr is not working." + withContext(Dispatchers.Main) { + context.toast(message) } - } - } catch (e: Exception) { - Log.e("FlareSolverrInterceptor", "Error: ${e.message}", e) - withContext(Dispatchers.Main) { - context.toast("Error contacting FlareSolverr") } } + } catch (e: Exception) { + Log.e("FlareSolverrInterceptor", "Error: ${e.message}", e) + withContext(Dispatchers.Main) { + context.toast("Error contacting FlareSolverr") + } + } } - private var job: Job? = null // SY <-- } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index a9cac1d4dda1..856f3a3d65d6 100755 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -31,7 +31,7 @@ class CloudflareInterceptor( override fun shouldIntercept(response: Response): Boolean { // Check if FlareSolverr is enabled if it's enabled we don't need to bypass Cloudflare through WebView - if (preferences.enableFlareSolverr().get()){ + if (preferences.enableFlareSolverr().get()) { return false } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt index beb68ad0d031..587913e764cd 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -136,7 +136,6 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int * is not found or considered invalid, suggesting that access may be blocked by Cloudflare. */ private fun isCfClearanceCookieValid(url: String): Boolean { - // Checks if the cf_clearance cookie is valid. This function has been updated to check for the cookie's presence across both the base domain and any subdomains. val baseDomain = getBaseDomain(url) val checkUrl = "https://www.$baseDomain/" val cookiesStringForBaseDomain = cookieManager.getCookie(checkUrl) From 9552544d5f57870d0d092d8e8aa054c18703e2b5 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Sun, 14 Apr 2024 09:37:07 +1000 Subject: [PATCH 08/11] refactor: use kotlinx, and avoid using org.JSON Signed-off-by: KaiserBh --- .../settings/screen/SettingsAdvancedScreen.kt | 76 ++--- .../tachiyomi/network/AndroidCookieJar.kt | 16 + .../interceptor/CloudflareInterceptor.kt | 4 +- .../interceptor/FlareSolverrInterceptor.kt | 314 +++++++++--------- .../commonMain/resources/MR/base/strings.xml | 3 + 5 files changed, 221 insertions(+), 192 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index 20c3e172121d..731c9f0edef5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -3,9 +3,7 @@ package eu.kanade.presentation.more.settings.screen import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent -import android.os.Build import android.provider.Settings -import android.util.Log import android.webkit.WebStorage import android.webkit.WebView import android.widget.Toast @@ -45,6 +43,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.PREF_DOH_360 import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS @@ -57,7 +56,9 @@ import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101 import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9 import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN -import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException +import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.interceptor.FlareSolverrInterceptor +import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.source.AndroidSourceManager import eu.kanade.tachiyomi.ui.more.OnboardingScreen import eu.kanade.tachiyomi.util.CrashLogUtil @@ -82,13 +83,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import logcat.LogPriority import okhttp3.Headers import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient -import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody -import org.json.JSONObject import tachiyomi.core.common.i18n.pluralStringResource import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchNonCancellable @@ -106,6 +107,7 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import java.io.File import tachiyomi.core.common.preference.Preference as BasePreference @@ -818,58 +820,50 @@ object SettingsAdvancedScreen : SearchableSettings { userAgentPref: BasePreference, context: android.content.Context ) { + val json: Json by injectLazy() + val jsonMediaType = "application/json".toMediaType() + val client = OkHttpClient.Builder().build() + try { withContext(Dispatchers.IO) { - val client = OkHttpClient() val flareSolverUrl = flareSolverrUrlPref.get().trim() - val mediaType = "application/json; charset=utf-8".toMediaType() - val data = JSONObject() - .put("cmd", "request.get") - .put("url", "https://www.google.com/") - .put("maxTimeout", 60000) - .put("returnOnlyCookies", true) - .toString() - val body = data.toRequestBody(mediaType) - val request = Request.Builder() - .url(flareSolverUrl) - .post(body) - .header("Content-Type", "application/json") - .build() - - Log.d("FlareSolverrRequest", "Sending request to FlareSolverr: $flareSolverUrl with payload: $data") + val flareSolverResponse = with(json) { + client.newCall( + POST( + url = flareSolverUrl, + body = + Json.encodeToString( + FlareSolverrInterceptor.CFClearance.FlareSolverRequest( + "request.get", + "https://www.google.com/", + returnOnlyCookies = true, + maxTimeout = 60000, + ), + ).toRequestBody(jsonMediaType), + ), + ).awaitSuccess().parseAs() + } - val response = client.newCall(request).execute() - if (!response.isSuccessful) { - Log.e("HttpError", "Request failed with status code: ${response.code}") - throw CloudflareBypassException("Failed with status code: ${response.code}") - } + if (flareSolverResponse.solution.status in 200..299) { + // Set the user agent to the one provided by FlareSolverr + userAgentPref.set(flareSolverResponse.solution.userAgent) - val responseBody = response.body.string() - Log.d("HttpResponse", responseBody) - - val jsonResponse = JSONObject(responseBody) - val status = jsonResponse.optString("status") - if (status == "ok") { - val newUserAgent = jsonResponse.optJSONObject("solution")?.getString("userAgent") - newUserAgent?.let { - userAgentPref.set(it) - Log.d("FlareSolverrInterceptor", "User agent updated to: $it") - } - val message = "FlareSolverr is working. User agent updated. Please restart the app" + val message = SYMR.strings.flare_solver_user_agent_update_success withContext(Dispatchers.Main) { context.toast(message) } } else { - val message = "FlareSolverr is not working." + val message = SYMR.strings.flare_solver_update_user_agent_failed withContext(Dispatchers.Main) { context.toast(message) } } } } catch (e: Exception) { - Log.e("FlareSolverrInterceptor", "Error: ${e.message}", e) + logcat (LogPriority.ERROR, tag = "FlareSolverr") + { "Failed to resolve with FlareSolverr: ${e.message}" } withContext(Dispatchers.Main) { - context.toast("Error contacting FlareSolverr") + context.toast(SYMR.strings.flare_solver_error) } } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt index f9322e840fed..e3884340913f 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt @@ -51,4 +51,20 @@ class AndroidCookieJar : CookieJar { fun removeAll() { manager.removeAllCookies {} } + + fun addAll(url: HttpUrl, cookies: List) { + val urlString = url.toString() + val existingCookies = manager.getCookie(urlString)?.split("; ")?.associate { + val (name, value) = it.split('=', limit = 2) + name to value + }?.toMutableMap() ?: mutableMapOf() + + cookies.forEach { newCookie -> + existingCookies[newCookie.name] = newCookie.value + } + + existingCookies.forEach { (name, value) -> + manager.setCookie(urlString, "$name=$value") + } + } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 856f3a3d65d6..adde65311fb0 100755 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -146,8 +146,8 @@ class CloudflareInterceptor( } } -private val ERROR_CODES = listOf(403, 503) -private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") +val ERROR_CODES = listOf(403, 503) +val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val COOKIE_NAMES = listOf("cf_clearance") class CloudflareBypassException(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt index 587913e764cd..c7e7b719afd8 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -1,23 +1,42 @@ package eu.kanade.tachiyomi.network.interceptor -import android.net.Uri import android.util.Log import android.webkit.CookieManager +import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.parseAs +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.Cookie +import okhttp3.HttpUrl import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response -import org.json.JSONObject +import okio.IOException +import uy.kohesive.injekt.injectLazy class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Interceptor { - private val cookieManager = CookieManager.getInstance() +// private val cookieManager = CookieManager.getInstance() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() + val originalResponse = chain.proceed(originalRequest) + + // Check if Cloudflare anti-bot is on + if (!(originalResponse.code in ERROR_CODES && originalResponse.header("Server") in SERVER_CHECK)) { + return originalResponse + } + // FlareSolverr is disabled, so just proceed with the request. if (!preferences.enableFlareSolverr().get()) { return chain.proceed(originalRequest) @@ -25,163 +44,160 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int Log.d("FlareSolverrInterceptor", "Intercepting request: ${originalRequest.url}") - // First, ensure cf_clearance for subdomains if needed. - ensureCfClearanceForSubdomain(originalRequest.url.toString()) - - var cookiesString = "" + return try { + originalResponse.close() - // Check if the cf_clearance cookie exists and is valid for the base domain or the current subdomain. - if (!isCfClearanceCookieValid(originalRequest.url.toString())) { - // If the cf_clearance cookie is missing or not valid, get the cookies string from FlareSolverr. - cookiesString = resolveWithFlareSolverr(originalRequest) - - // If cookies were found, parse and add them to the cookie jar. - cookiesString.takeIf { it.isNotBlank() }?.let { cookies -> - val url = originalRequest.url.toString() - cookies.split("; ").forEach { cookie -> - Log.d("FlareSolverrInterceptor", "Adding cookie: $cookie, to $url") - cookieManager.setCookie(url, cookie) + val request = + runBlocking { + CFClearance.resolveWithFlareSolverr(originalRequest) } - } - } else { - // Here, the cf_clearance cookie is valid; we need to ensure it's included in the request for subdomains. - val cfClearanceValue = getCookieValueForDomain(originalRequest.url.toString(), CF_COOKIE_NAME) - if (cfClearanceValue != null) { - cookiesString = "cf_clearance=$cfClearanceValue" - } - } - // Create a new request with the cookies, whether they are newly obtained or retrieved from the cookie jar. - val newRequest = if (cookiesString.isNotBlank()) { - originalRequest.newBuilder() - .header("Cookie", cookiesString.trimEnd(';')) - .build() - } else { - originalRequest + chain.proceed(request) + } catch (e: Exception) { + // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that + // we don't crash the entire app + throw IOException(e) } - - return chain.proceed(newRequest) } - private fun resolveWithFlareSolverr(originalRequest: Request, addAllCookies: Boolean = true): String { - try { - val client = OkHttpClient() - - val flareSolverUrl = preferences.flareSolverrUrl().get().trim() - val mediaType = "application/json; charset=utf-8".toMediaType() - val data = JSONObject() - .put("cmd", "request.get") - .put("url", originalRequest.url.toString()) - .put("maxTimeout", 60000) - .put("returnOnlyCookies", true) - .toString() - val body = data.toRequestBody(mediaType) - val request = Request.Builder() - .url(flareSolverUrl) - .post(body) - .header("Content-Type", "application/json") - .build() - - Log.d("FlareSolverrRequest", "Sending request to FlareSolverr: $flareSolverUrl with payload: $data") - - val response = client.newCall(request).execute() - if (!response.isSuccessful) { - Log.e("HttpError", "Request failed with status code: ${response.code}") - throw CloudflareBypassException("Failed with status code: ${response.code}") - } + object CFClearance { + private val network: NetworkHelper by injectLazy() + private val json: Json by injectLazy() + private val jsonMediaType = "application/json".toMediaType() + private val networkPreferences: NetworkPreferences by injectLazy() + private val flareSolverrUrl = networkPreferences.flareSolverrUrl().get() + private val mutex = Mutex() + + @Serializable + data class FlareSolverCookie( + val name: String, + val value: String, + ) + + @Serializable + data class FlareSolverRequest( + val cmd: String, + val url: String, + val maxTimeout: Int? = null, + val session: List? = null, + @SerialName("session_ttl_minutes") + val sessionTtlMinutes: Int? = null, + val cookies: List? = null, + val returnOnlyCookies: Boolean? = null, + val proxy: String? = null, + val postData: String? = null, // only used with cmd 'request.post' + ) + + @Serializable + data class FlareSolverSolutionCookie( + val name: String, + val value: String, + val domain: String, + val path: String, + val expires: Double? = null, + val size: Int? = null, + val httpOnly: Boolean, + val secure: Boolean, + val session: Boolean? = null, + val sameSite: String, + ) + + @Serializable + data class FlareSolverSolution( + val url: String, + val status: Int, + val headers: Map? = null, + val response: String? = null, + val cookies: List, + val userAgent: String, + ) + + @Serializable + data class FlareSolverResponse( + val solution: FlareSolverSolution, + val status: String, + val message: String, + val startTimestamp: Long, + val endTimestamp: Long, + val version: String, + ) + + suspend fun resolveWithFlareSolverr( + originalRequest: Request, + ): Request { + Log.d("FlareSolverr", "Requesting challenge solution for ${originalRequest.url}") + + val flareSolverResponse = + with(json) { + mutex.withLock { + network.client.newCall( + POST( + url = flareSolverrUrl, + body = + Json.encodeToString( + FlareSolverRequest( + "request.get", + originalRequest.url.toString(), + cookies = + network.cookieJar.get(originalRequest.url).map { + FlareSolverCookie(it.name, it.value) + }, + returnOnlyCookies = true, + maxTimeout = 30000, + ), + ).toRequestBody(jsonMediaType), + ), + ).awaitSuccess().parseAs() + } + } - val responseBody = response.body.string() - Log.d("HttpResponse", responseBody) - - val jsonResponse = JSONObject(responseBody) - val status = jsonResponse.optString("status") - if (status == "ok") { - val solution = jsonResponse.optJSONObject("solution") - val cookiesArray = solution?.optJSONArray("cookies") - - val cookieStringBuilder = StringBuilder() - cookiesArray?.let { - for (i in 0 until it.length()) { - val cookieObj = it.getJSONObject(i) - // Check if we should add all cookies or just cf_clearance - if (addAllCookies || cookieObj.getString("name") == "cf_clearance") { - val cookieString = "${cookieObj.getString("name")}=${cookieObj.getString("value")};" - cookieStringBuilder.append(cookieString) - if (i < cookiesArray.length() - 1) { - cookieStringBuilder.append(" ") - } + if (flareSolverResponse.solution.status in 200..299) { + + Log.d("FlareSolverr", "Received challenge solution for ${originalRequest.url}") + + val cookies = + flareSolverResponse.solution.cookies + .map { cookie -> + Cookie.Builder() + .name(cookie.name) + .value(cookie.value) + .domain(cookie.domain) + .path(cookie.path) + .expiresAt(cookie.expires?.takeUnless { it < 0.0 }?.toLong() ?: Long.MAX_VALUE) + .also { + if (cookie.httpOnly) it.httpOnly() + if (cookie.secure) it.secure() + } + .build() } + .groupBy { it.domain } + .flatMap { (domain, cookies) -> + network.cookieJar.addAll( + HttpUrl.Builder() + .scheme("http") + .host(domain.removePrefix(".")) + .build(), + cookies, + ) + + cookies + } + Log.d("FlareSolverr", "New cookies\n${cookies.joinToString("; ")}" ) + val finalCookies = + network.cookieJar.get(originalRequest.url).joinToString("; ", postfix = "; ") { + "${it.name}=${it.value}" } - } - return cookieStringBuilder.toString().trimEnd() + Log.d ("FlareSolverr", "Final cookies\n$finalCookies") + return originalRequest.newBuilder() + .header("Cookie", finalCookies) + .header("User-Agent", flareSolverResponse.solution.userAgent) + .build() } else { - throw CloudflareBypassException("Failed to solve challenge: $status") + Log.d("FlareSolverr", "Failed to solve challenge: ${flareSolverResponse.message}") + throw CloudflareBypassException() } - } catch (e: Exception) { - Log.e("HttpError", "Failed to resolve with FlareSolverr: ${e.message}", e) - if (e is CloudflareBypassException) throw e - throw CloudflareBypassException("Error resolving with FlareSolverr", e) } - } - /** - * Checks if the `cf_clearance` cookie is present and valid for the base domain of the provided URL. - * This is a critical check when accessing Cloudflare-protected sites, as the presence of this cookie - * suggests that Cloudflare's challenge has already been solved for this domain. The method extends - * this check to cover both the base domain and any subdomains, ensuring comprehensive coverage. - * - * @param url The URL as a [String] for which to verify the presence of the `cf_clearance` cookie. - * @return [Boolean] `true` if the `cf_clearance` cookie is found for the base domain of the URL, - * indicating that Cloudflare's challenge page might not be triggered. Returns `false` if the cookie - * is not found or considered invalid, suggesting that access may be blocked by Cloudflare. - */ - private fun isCfClearanceCookieValid(url: String): Boolean { - val baseDomain = getBaseDomain(url) - val checkUrl = "https://www.$baseDomain/" - val cookiesStringForBaseDomain = cookieManager.getCookie(checkUrl) - val hasCfClearanceCookie = cookiesStringForBaseDomain?.contains("cf_clearance=") ?: false - Log.d("CookieManager", "cf_clearance cookie for $checkUrl: $hasCfClearanceCookie") - return hasCfClearanceCookie - } - - /** - * Ensures that if a valid `cf_clearance` cookie exists for the base domain, it is also set for - * any subdomains accessed. This method is useful for scenarios where a Cloudflare-protected - * site's base domain has passed Cloudflare's challenge, and subsequent requests to its subdomains - * should inherit this clearance without needing to solve the challenge again. - * - * The method checks for the `cf_clearance` cookie's presence and validity for the base domain. - * If found and valid, it sets the same `cf_clearance` cookie for the subdomain of the URL provided. - * - * @param url The URL as a [String] indicating the subdomain for which the `cf_clearance` cookie - * should be set, assuming it's valid for the base domain. - */ - private fun ensureCfClearanceForSubdomain(url: String) { - val uri = Uri.parse(url) - val host = uri.host ?: return - val baseDomain = getBaseDomain(url) - if (host != "www.$baseDomain" && isCfClearanceCookieValid("https://www.$baseDomain/")) { - val cfClearanceValue = getCookieValueForDomain("https://www.$baseDomain/", CF_COOKIE_NAME) - if (cfClearanceValue != null) { - val subdomainUrl = "https://$host/" - cookieManager.setCookie(subdomainUrl, "cf_clearance=$cfClearanceValue") - Log.d("CookieManager", "Set cf_clearance for $subdomainUrl: cf_clearance=$cfClearanceValue") - } - } - } - - private fun getBaseDomain(url: String): String { - val uri = Uri.parse(url) - val host = uri.host ?: return "" - val parts = host.split(".") - return if (parts.size >= 2) parts.takeLast(2).joinToString(".") else host - } - - private fun getCookieValueForDomain(url: String, cookieName: String): String? { - val cookiesString = cookieManager.getCookie(url) - return cookiesString?.split("; ")?.firstOrNull { it.startsWith("$cookieName=") } - ?.substringAfter("$cookieName=") + private class CloudflareBypassException : Exception() } } - -private const val CF_COOKIE_NAME = "cf_clearance" diff --git a/i18n-sy/src/commonMain/resources/MR/base/strings.xml b/i18n-sy/src/commonMain/resources/MR/base/strings.xml index b4fee304c6e0..fbd367ed1d92 100644 --- a/i18n-sy/src/commonMain/resources/MR/base/strings.xml +++ b/i18n-sy/src/commonMain/resources/MR/base/strings.xml @@ -153,6 +153,9 @@ Bandwidth Hero Proxy Server Put Bandwidth Hero Proxy server url here Keep entries with read chapters + FlareSolverr is working. User agent updated. Please restart the app + FlareSolverr is not working. User agent not updated. Please check your settings + Error contacting FlareSolverr Minimal From 89023559666c1d45619d78b6ce498526fd9ab464 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Fri, 7 Jun 2024 00:22:27 +1000 Subject: [PATCH 09/11] chore: fix? So this kinda works, it sets the cookie in the jar but for some reason fails to retrieve it. But if I directly use CookieManager.getInstance() It works fine?? --- .../tachiyomi/network/AndroidCookieJar.kt | 27 ++++++- .../interceptor/FlareSolverrInterceptor.kt | 77 ++++++++++--------- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt index e3884340913f..b153e3442f9c 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.network +import android.util.Log import android.webkit.CookieManager import okhttp3.Cookie import okhttp3.CookieJar @@ -54,17 +55,37 @@ class AndroidCookieJar : CookieJar { fun addAll(url: HttpUrl, cookies: List) { val urlString = url.toString() + Log.d("AndroidCookieJar", "Adding cookies to URL: $urlString") + + // Log incoming cookies to add + cookies.forEach { newCookie -> + Log.d("AndroidCookieJar", "Incoming cookie: ${newCookie.name}=${newCookie.value}") + } + + // Get existing cookies for the URL val existingCookies = manager.getCookie(urlString)?.split("; ")?.associate { val (name, value) = it.split('=', limit = 2) name to value }?.toMutableMap() ?: mutableMapOf() + Log.d("AndroidCookieJar", "Existing cookies: $existingCookies") + + // Add or update the cookies cookies.forEach { newCookie -> + Log.d("AndroidCookieJar", "Adding/updating cookie: ${newCookie.name}=${newCookie.value}") existingCookies[newCookie.name] = newCookie.value } - existingCookies.forEach { (name, value) -> - manager.setCookie(urlString, "$name=$value") - } + // Convert the map back to a string and set it in the cookie manager + val finalCookiesString = existingCookies.entries.joinToString("; ") { "${it.key}=${it.value}" } + Log.d("AndroidCookieJar", "Final cookies string: $finalCookiesString") + manager.setCookie(urlString, finalCookiesString) + + // Verify if cookies are set correctly + val setCookies = manager.getCookie(urlString) + Log.d("AndroidCookieJar", "Set cookies in manager: $setCookies") + + Log.d("AndroidCookieJar", "All cookies added for URL: $urlString") } + } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt index c7e7b719afd8..c76e6bffc8b4 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.network.interceptor import android.util.Log -import android.webkit.CookieManager import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.POST @@ -25,8 +24,6 @@ import okio.IOException import uy.kohesive.injekt.injectLazy class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Interceptor { -// private val cookieManager = CookieManager.getInstance() - override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() @@ -125,7 +122,9 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int suspend fun resolveWithFlareSolverr( originalRequest: Request, ): Request { - Log.d("FlareSolverr", "Requesting challenge solution for ${originalRequest.url}") + val flareSolverTag = "FlareSolverr" + + Log.d( flareSolverTag, "Requesting challenge solution for ${originalRequest.url}") val flareSolverResponse = with(json) { @@ -152,48 +151,54 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int } if (flareSolverResponse.solution.status in 200..299) { + Log.d(flareSolverTag, "Received challenge solution for ${originalRequest.url}") + Log.d(flareSolverTag, "Received cookies from FlareSolverr\n${flareSolverResponse.solution.cookies.joinToString("; ")}") + + val cookies = flareSolverResponse.solution.cookies.map { cookie -> + Log.d(flareSolverTag, "Creating cookie for ${cookie.name}") + try { + val domain = cookie.domain.removePrefix(".") + val builder = Cookie.Builder() + .name(cookie.name) + .value(cookie.value) + .domain(domain) + .path(cookie.path) + .expiresAt(cookie.expires?.takeUnless { it < 0.0 }?.toLong() ?: Long.MAX_VALUE) + if (cookie.httpOnly) builder.httpOnly() + if (cookie.secure) builder.secure() + val builtCookie = builder.build() + Log.d(flareSolverTag, "Built cookie: $builtCookie") + builtCookie + } catch (e: Exception) { + Log.e(flareSolverTag, "Error creating cookie for ${cookie.name}", e) + throw e + } + }.groupBy { it.domain }.flatMap { (domain, cookies) -> + Log.d(flareSolverTag, "Adding cookies to domain $domain") + network.cookieJar.addAll( + HttpUrl.Builder() + .scheme("https") + .host(domain.removePrefix(".")) + .build(), + cookies + ) + cookies + } + + Log.d(flareSolverTag, "New cookies\n${cookies.joinToString("; ")}") - Log.d("FlareSolverr", "Received challenge solution for ${originalRequest.url}") - - val cookies = - flareSolverResponse.solution.cookies - .map { cookie -> - Cookie.Builder() - .name(cookie.name) - .value(cookie.value) - .domain(cookie.domain) - .path(cookie.path) - .expiresAt(cookie.expires?.takeUnless { it < 0.0 }?.toLong() ?: Long.MAX_VALUE) - .also { - if (cookie.httpOnly) it.httpOnly() - if (cookie.secure) it.secure() - } - .build() - } - .groupBy { it.domain } - .flatMap { (domain, cookies) -> - network.cookieJar.addAll( - HttpUrl.Builder() - .scheme("http") - .host(domain.removePrefix(".")) - .build(), - cookies, - ) - - cookies - } - Log.d("FlareSolverr", "New cookies\n${cookies.joinToString("; ")}" ) val finalCookies = network.cookieJar.get(originalRequest.url).joinToString("; ", postfix = "; ") { "${it.name}=${it.value}" } - Log.d ("FlareSolverr", "Final cookies\n$finalCookies") + + Log.d(flareSolverTag, "Final cookies\n$finalCookies") return originalRequest.newBuilder() .header("Cookie", finalCookies) .header("User-Agent", flareSolverResponse.solution.userAgent) .build() } else { - Log.d("FlareSolverr", "Failed to solve challenge: ${flareSolverResponse.message}") + Log.d(flareSolverTag, "Failed to solve challenge: ${flareSolverResponse.message}") throw CloudflareBypassException() } } From edf35a0a3655cf2a832fa7dd2c7b5cd5028a0bbb Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Fri, 7 Jun 2024 00:32:13 +1000 Subject: [PATCH 10/11] fix: working version. So this is the working version using CookieManager Directly. --- .../interceptor/FlareSolverrInterceptor.kt | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt index c76e6bffc8b4..58f065cdd384 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.network.interceptor import android.util.Log +import android.webkit.CookieManager import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.POST @@ -22,6 +23,9 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okio.IOException import uy.kohesive.injekt.injectLazy +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.Date class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { @@ -121,6 +125,7 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int suspend fun resolveWithFlareSolverr( originalRequest: Request, + cookieManager: CookieManager = CookieManager.getInstance(), ): Request { val flareSolverTag = "FlareSolverr" @@ -154,47 +159,31 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int Log.d(flareSolverTag, "Received challenge solution for ${originalRequest.url}") Log.d(flareSolverTag, "Received cookies from FlareSolverr\n${flareSolverResponse.solution.cookies.joinToString("; ")}") - val cookies = flareSolverResponse.solution.cookies.map { cookie -> + flareSolverResponse.solution.cookies.forEach { cookie -> Log.d(flareSolverTag, "Creating cookie for ${cookie.name}") try { val domain = cookie.domain.removePrefix(".") - val builder = Cookie.Builder() - .name(cookie.name) - .value(cookie.value) - .domain(domain) - .path(cookie.path) - .expiresAt(cookie.expires?.takeUnless { it < 0.0 }?.toLong() ?: Long.MAX_VALUE) - if (cookie.httpOnly) builder.httpOnly() - if (cookie.secure) builder.secure() - val builtCookie = builder.build() - Log.d(flareSolverTag, "Built cookie: $builtCookie") - builtCookie + val cookieString = buildCookieString(cookie, domain) + Log.d(flareSolverTag, "Adding cookie string to CookieManager: $cookieString") + cookieManager.setCookie("https://$domain", cookieString) } catch (e: Exception) { Log.e(flareSolverTag, "Error creating cookie for ${cookie.name}", e) throw e } - }.groupBy { it.domain }.flatMap { (domain, cookies) -> - Log.d(flareSolverTag, "Adding cookies to domain $domain") - network.cookieJar.addAll( - HttpUrl.Builder() - .scheme("https") - .host(domain.removePrefix(".")) - .build(), - cookies - ) - cookies } - Log.d(flareSolverTag, "New cookies\n${cookies.joinToString("; ")}") + // Verify if the cookies are set correctly + val allCookies = flareSolverResponse.solution.cookies.mapNotNull { cookie -> + val domain = cookie.domain.removePrefix(".") + val setCookie = cookieManager.getCookie("https://$domain") + Log.d(flareSolverTag, "Set cookies in CookieManager for $domain: $setCookie") + setCookie + }.joinToString("; ") - val finalCookies = - network.cookieJar.get(originalRequest.url).joinToString("; ", postfix = "; ") { - "${it.name}=${it.value}" - } + Log.d(flareSolverTag, "Final cookies\n$allCookies") - Log.d(flareSolverTag, "Final cookies\n$finalCookies") return originalRequest.newBuilder() - .header("Cookie", finalCookies) + .header("Cookie", allCookies) .header("User-Agent", flareSolverResponse.solution.userAgent) .build() } else { @@ -203,6 +192,22 @@ class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Int } } + private fun buildCookieString(cookie: FlareSolverSolutionCookie, domain: String): String { + val formatter = DateTimeFormatter.RFC_1123_DATE_TIME + val expires = if (cookie.expires != null && cookie.expires > 0) { + ZonedDateTime.now().plusSeconds(cookie.expires.toLong()).format(formatter) + } else { + "Fri, 31 Dec 9999 23:59:59 GMT" + } + + return StringBuilder().apply { + append("${cookie.name}=${cookie.value}; Domain=$domain; Path=${cookie.path}; Expires=$expires;") + if (cookie.httpOnly) append(" HttpOnly;") + if (cookie.secure) append(" Secure;") + }.toString() + } + + private class CloudflareBypassException : Exception() } } From 022f2a45405c7a8fef029b067502951e3b30c019 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Fri, 7 Jun 2024 00:41:18 +1000 Subject: [PATCH 11/11] chore: optimize imports. --- .../tachiyomi/network/interceptor/FlareSolverrInterceptor.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt index 58f065cdd384..13f81aadf56c 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/FlareSolverrInterceptor.kt @@ -14,8 +14,6 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import okhttp3.Cookie -import okhttp3.HttpUrl import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request @@ -25,7 +23,6 @@ import okio.IOException import uy.kohesive.injekt.injectLazy import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import java.util.Date class FlareSolverrInterceptor(private val preferences: NetworkPreferences) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response {