diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
index 3b318803..ff0860fc 100644
--- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
+++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
@@ -23,6 +23,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.IOException
@@ -56,11 +57,38 @@ class CloudflareInterceptor(
originalResponse.close()
// network.cookieStore.remove(originalRequest.url.toUri())
- val request =
+ val flareResponseFallback = serverConfig.flareSolverrAsResponseFallback.value
+ val flareResponse =
runBlocking {
- CFClearance.resolveWithFlareSolverr(setUserAgent, originalRequest)
+ CFClearance.resolveWithFlareSolver(originalRequest, !flareResponseFallback)
}
+ if (flareResponse.message.contains("not detected", ignoreCase = true)) {
+ logger.debug { "FlareSolverr failed to detect Cloudflare challenge" }
+
+ if (flareResponseFallback &&
+ flareResponse.solution.status in 200..299 &&
+ flareResponse.solution.response != null
+ ) {
+ val isImage = flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
+ if (!isImage) {
+ logger.debug { "Falling back to FlareSolverr response" }
+
+ setUserAgent(flareResponse.solution.userAgent)
+
+ return originalResponse
+ .newBuilder()
+ .code(flareResponse.solution.status)
+ .body(flareResponse.solution.response.toResponseBody())
+ .build()
+ } else {
+ logger.debug { "FlareSolverr response is an image html template, not falling back" }
+ }
+ }
+ }
+
+ val request = CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
+
chain.proceed(request)
} catch (e: Exception) {
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
@@ -73,6 +101,7 @@ class CloudflareInterceptor(
private val ERROR_CODES = listOf(403, 503)
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance")
+ private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""
(.*?) \(\d+×\d+\)""")
}
}
@@ -153,37 +182,43 @@ object CFClearance {
val version: String,
)
- suspend fun resolveWithFlareSolverr(
+ suspend fun resolveWithFlareSolver(
+ originalRequest: Request,
+ onlyCookies: Boolean,
+ ): FlareSolverResponse {
+ val timeout = serverConfig.flareSolverrTimeout.value.seconds
+
+ return with(json) {
+ mutex.withLock {
+ client.value.newCall(
+ POST(
+ url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1",
+ body =
+ Json.encodeToString(
+ FlareSolverRequest(
+ "request.get",
+ originalRequest.url.toString(),
+ session = serverConfig.flareSolverrSessionName.value,
+ sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
+ cookies =
+ network.cookieStore.get(originalRequest.url).map {
+ FlareSolverCookie(it.name, it.value)
+ },
+ returnOnlyCookies = onlyCookies,
+ maxTimeout = timeout.inWholeMilliseconds.toInt(),
+ ),
+ ).toRequestBody(jsonMediaType),
+ ),
+ ).awaitSuccess().parseAs()
+ }
+ }
+ }
+
+ fun requestWithFlareSolverr(
+ flareSolverResponse: FlareSolverResponse,
setUserAgent: (String) -> Unit,
originalRequest: Request,
): Request {
- val timeout = serverConfig.flareSolverrTimeout.value.seconds
- val flareSolverResponse =
- with(json) {
- mutex.withLock {
- client.value.newCall(
- POST(
- url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1",
- body =
- Json.encodeToString(
- FlareSolverRequest(
- "request.get",
- originalRequest.url.toString(),
- session = serverConfig.flareSolverrSessionName.value,
- sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
- cookies =
- network.cookieStore.get(originalRequest.url).map {
- FlareSolverCookie(it.name, it.value)
- },
- returnOnlyCookies = true,
- maxTimeout = timeout.inWholeMilliseconds.toInt(),
- ),
- ).toRequestBody(jsonMediaType),
- ),
- ).awaitSuccess().parseAs()
- }
- }
-
if (flareSolverResponse.solution.status in 200..299) {
setUserAgent(flareSolverResponse.solution.userAgent)
val cookies =
diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt
index fc083ca1..325f749f 100644
--- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt
+++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt
@@ -98,6 +98,7 @@ class SettingsMutation {
updateSetting(settings.flareSolverrTimeout, serverConfig.flareSolverrTimeout)
updateSetting(settings.flareSolverrSessionName, serverConfig.flareSolverrSessionName)
updateSetting(settings.flareSolverrSessionTtl, serverConfig.flareSolverrSessionTtl)
+ updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback)
}
fun setSettings(input: SetSettingsInput): SetSettingsPayload {
diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt
index a980d5f4..8e0c0978 100644
--- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt
+++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt
@@ -89,6 +89,7 @@ interface Settings : Node {
val flareSolverrTimeout: Int?
val flareSolverrSessionName: String?
val flareSolverrSessionTtl: Int?
+ val flareSolverrAsResponseFallback: Boolean?
}
data class PartialSettingsType(
@@ -151,6 +152,7 @@ data class PartialSettingsType(
override val flareSolverrTimeout: Int?,
override val flareSolverrSessionName: String?,
override val flareSolverrSessionTtl: Int?,
+ override val flareSolverrAsResponseFallback: Boolean?,
) : Settings
class SettingsType(
@@ -213,6 +215,7 @@ class SettingsType(
override val flareSolverrTimeout: Int,
override val flareSolverrSessionName: String,
override val flareSolverrSessionTtl: Int,
+ override val flareSolverrAsResponseFallback: Boolean,
) : Settings {
constructor(config: ServerConfig = serverConfig) : this(
config.ip.value,
@@ -270,5 +273,6 @@ class SettingsType(
config.flareSolverrTimeout.value,
config.flareSolverrSessionName.value,
config.flareSolverrSessionTtl.value,
+ config.flareSolverrAsResponseFallback.value,
)
}
diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt
index 21b7c7f3..71655d57 100644
--- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt
+++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt
@@ -141,6 +141,7 @@ class ServerConfig(getConfig: () -> Config, val moduleName: String = SERVER_CONF
val flareSolverrTimeout: MutableStateFlow by OverrideConfigValue(IntConfigAdapter)
val flareSolverrSessionName: MutableStateFlow by OverrideConfigValue(StringConfigAdapter)
val flareSolverrSessionTtl: MutableStateFlow by OverrideConfigValue(IntConfigAdapter)
+ val flareSolverrAsResponseFallback: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter)
@OptIn(ExperimentalCoroutinesApi::class)
fun subscribeTo(
diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf
index 0cf00c4f..dee08efb 100644
--- a/server/src/main/resources/server-reference.conf
+++ b/server/src/main/resources/server-reference.conf
@@ -67,3 +67,4 @@ server.flareSolverrUrl = "http://localhost:8191"
server.flareSolverrTimeout = 60 # time in seconds
server.flareSolverrSessionName = "suwayomi"
server.flareSolverrSessionTtl = 15 # time in minutes
+server.flareSolverrAsResponseFallback = false
diff --git a/server/src/test/resources/server-reference.conf b/server/src/test/resources/server-reference.conf
index 0cf00c4f..dee08efb 100644
--- a/server/src/test/resources/server-reference.conf
+++ b/server/src/test/resources/server-reference.conf
@@ -67,3 +67,4 @@ server.flareSolverrUrl = "http://localhost:8191"
server.flareSolverrTimeout = 60 # time in seconds
server.flareSolverrSessionName = "suwayomi"
server.flareSolverrSessionTtl = 15 # time in minutes
+server.flareSolverrAsResponseFallback = false