Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add FlareSolverr to bypass cloudflare #1124

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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.webkit.WebStorage
import android.webkit.WebView
Expand Down Expand Up @@ -44,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
Expand All @@ -56,6 +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.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
Expand All @@ -76,10 +79,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 kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import logcat.LogPriority
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchNonCancellable
Expand All @@ -97,7 +107,9 @@ 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

object SettingsAdvancedScreen : SearchableSettings {

Expand Down Expand Up @@ -234,9 +246,13 @@ object SettingsAdvancedScreen : SearchableSettings {
): Preference.PreferenceGroup {
val context = LocalContext.current
val networkHelper = remember { Injekt.get<NetworkHelper>() }
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),
Expand Down Expand Up @@ -315,6 +331,28 @@ 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,
subtitle = stringResource(MR.strings.pref_test_flare_solverr_and_update_user_agent_summary),
onClick = {
scope.launch {
testFlareSolverrAndUpdateUserAgent(flareSolverrUrlPref, userAgentPref, context)
}
},
)

),
)
}
Expand Down Expand Up @@ -777,6 +815,59 @@ object SettingsAdvancedScreen : SearchableSettings {
)
}

private suspend fun testFlareSolverrAndUpdateUserAgent(
flareSolverrUrlPref: BasePreference<String>,
userAgentPref: BasePreference<String>,
context: android.content.Context
) {
val json: Json by injectLazy()
val jsonMediaType = "application/json".toMediaType()
val client = OkHttpClient.Builder().build()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the networkManager http client here?


try {
withContext(Dispatchers.IO) {
val flareSolverUrl = flareSolverrUrlPref.get().trim()
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<FlareSolverrInterceptor.CFClearance.FlareSolverResponse>()
}

if (flareSolverResponse.solution.status in 200..299) {
// Set the user agent to the one provided by FlareSolverr
userAgentPref.set(flareSolverResponse.solution.userAgent)

val message = SYMR.strings.flare_solver_user_agent_update_success
withContext(Dispatchers.Main) {
context.toast(message)
}
} else {
val message = SYMR.strings.flare_solver_update_user_agent_failed
withContext(Dispatchers.Main) {
context.toast(message)
}
}
}
} catch (e: Exception) {
logcat (LogPriority.ERROR, tag = "FlareSolverr")
{ "Failed to resolve with FlareSolverr: ${e.message}" }
withContext(Dispatchers.Main) {
context.toast(SYMR.strings.flare_solver_error)
}
}
}

private var job: Job? = null
// SY <--
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.network

import android.util.Log
import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
Expand Down Expand Up @@ -51,4 +52,40 @@ class AndroidCookieJar : CookieJar {
fun removeAll() {
manager.removeAllCookies {}
}

fun addAll(url: HttpUrl, cookies: List<Cookie>) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove debug logs if you are done with them, cookies shouldn't be logged if possible

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
}

// 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")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,6 +42,7 @@ open /* SY <-- */ class NetworkHelper(
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
.addNetworkInterceptor(IgnoreGzipInterceptor())
.addNetworkInterceptor(BrotliInterceptor)
.addNetworkInterceptor(FlareSolverrInterceptor(preferences))

if (isDebugBuild) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
Expand All @@ -50,7 +52,7 @@ open /* SY <-- */ class NetworkHelper(
}

builder.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
CloudflareInterceptor(context, cookieJar, preferences, ::defaultUserAgentProvider),
)

when (preferences.dohProvider().get()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ class NetworkPreferences(
return preferenceStore.getBoolean("verbose_logging", verboseLogging)
}

fun enableFlareSolverr(): Preference<Boolean> {
return preferenceStore.getBoolean("enable_flare_solverr", false)
}

fun flareSolverrUrl(): Preference<String> {
return preferenceStore.getString("flare_solverr_url", "http://localhost:8191/v1")
}

fun dohProvider(): Preference<Int> {
return preferenceStore.getInt("doh_provider", -1)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,12 +23,18 @@ import java.util.concurrent.CountDownLatch
class CloudflareInterceptor(
private val context: Context,
private val cookieManager: AndroidCookieJar,
private val preferences: NetworkPreferences,
defaultUserAgentProvider: () -> String,
) : WebViewInterceptor(context, defaultUserAgentProvider) {

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
}
Expand Down Expand Up @@ -134,13 +141,13 @@ class CloudflareInterceptor(
context.toast(MR.strings.information_webview_outdated, Toast.LENGTH_LONG)
}

throw CloudflareBypassException()
throw CloudflareBypassException("Error resolving with WebView")
}
}
}

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")

private class CloudflareBypassException : Exception()
class CloudflareBypassException(message: String, cause: Throwable? = null) : Exception(message, cause)
Loading
Loading