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 8 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 @@ -5,6 +5,7 @@ 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
Expand Down Expand Up @@ -54,6 +55,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
Expand All @@ -74,10 +76,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
Expand All @@ -96,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 {

Expand Down Expand Up @@ -249,9 +259,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 @@ -330,6 +344,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 @@ -764,6 +800,67 @@ object SettingsAdvancedScreen : SearchableSettings {
)
}

private suspend fun testFlareSolverrAndUpdateUserAgent(
flareSolverrUrlPref: BasePreference<String>,
userAgentPref: BasePreference<String>,
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()
kaiserbh marked this conversation as resolved.
Show resolved Hide resolved
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")
kaiserbh marked this conversation as resolved.
Show resolved Hide resolved

val response = client.newCall(request).execute()
kaiserbh marked this conversation as resolved.
Show resolved Hide resolved
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 <--
}
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,7 +141,7 @@ class CloudflareInterceptor(
context.toast(MR.strings.information_webview_outdated, Toast.LENGTH_LONG)
}

throw CloudflareBypassException()
throw CloudflareBypassException("Error resolving with WebView")
}
}
}
Expand All @@ -143,4 +150,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)
Loading
Loading