mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-06-30 09:24:34 -05:00
WebView based cloudflare interceptor (#456)
* WebView based cloudflare interceptor ported https://github.com/vvanglro/cf-clearance to kotlin * code clean up * Forgot to commit these * Get ResolveWithWebView working 1. Make sure to .use all closeable resources 2. Use 10 seconds instead of 1 second for waiting for cloudflare(this was the most probable issue) 3. Use Extension UA when possible 4. Minor cleanup of logging * rewrite and refactor Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
@@ -18,8 +18,8 @@ allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven("https://jitpack.io")
|
||||
maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/")
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ dependencies {
|
||||
implementation("com.github.junrar:junrar:7.5.3")
|
||||
|
||||
// CloudflareInterceptor
|
||||
implementation("net.sourceforge.htmlunit:htmlunit:2.65.1")
|
||||
implementation("com.microsoft.playwright:playwright:1.28.0")
|
||||
|
||||
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
|
||||
implementation("org.bouncycastle:bcprov-jdk18on:1.72")
|
||||
|
||||
@@ -39,6 +39,7 @@ class NetworkHelper(context: Context) {
|
||||
.cookieJar(cookieManager)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.callTimeout(2, TimeUnit.MINUTES)
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
|
||||
if (serverConfig.debugLogsEnabled) {
|
||||
|
||||
@@ -62,7 +62,7 @@ suspend fun Call.await(): Response {
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful) {
|
||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||
continuation.resumeWithException(HttpException(response.code))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
||||
.doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,3 +136,5 @@ inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(responseBody)
|
||||
}
|
||||
}
|
||||
|
||||
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import com.gargoylesoftware.htmlunit.BrowserVersion
|
||||
import com.gargoylesoftware.htmlunit.WebClient
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||
import com.microsoft.playwright.Browser
|
||||
import com.microsoft.playwright.BrowserType.LaunchOptions
|
||||
import com.microsoft.playwright.Page
|
||||
import com.microsoft.playwright.Playwright
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.interceptor.CFClearance.resolveWithWebView
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import suwayomi.tachidesk.server.ServerConfig
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
// from TachiWeb-Server
|
||||
class CloudflareInterceptor : Interceptor {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@@ -25,20 +30,22 @@ class CloudflareInterceptor : Interceptor {
|
||||
|
||||
logger.trace { "CloudflareInterceptor is being used." }
|
||||
|
||||
val response = chain.proceed(originalRequest)
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
|
||||
return response
|
||||
if (!(originalResponse.code in ERROR_CODES && originalResponse.header("Server") in SERVER_CHECK)) {
|
||||
return originalResponse
|
||||
}
|
||||
|
||||
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
|
||||
|
||||
return try {
|
||||
response.close()
|
||||
originalResponse.close()
|
||||
network.cookies.remove(originalRequest.url.toUri())
|
||||
|
||||
chain.proceed(resolveChallenge(response))
|
||||
val request = resolveWithWebView(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
|
||||
@@ -46,65 +53,148 @@ class CloudflareInterceptor : Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveChallenge(response: Response): Request {
|
||||
val browserVersion = BrowserVersion.BrowserVersionBuilder(BrowserVersion.BEST_SUPPORTED)
|
||||
.setUserAgent(response.request.header("User-Agent") ?: BrowserVersion.BEST_SUPPORTED.userAgent)
|
||||
.build()
|
||||
val convertedCookies = WebClient(browserVersion).use { webClient ->
|
||||
webClient.options.isThrowExceptionOnFailingStatusCode = false
|
||||
webClient.options.isThrowExceptionOnScriptError = false
|
||||
webClient.getPage<HtmlPage>(response.request.url.toString())
|
||||
webClient.waitForBackgroundJavaScript(10000)
|
||||
// Challenge solved, process cookies
|
||||
webClient.cookieManager.cookies.filter {
|
||||
// Only include Cloudflare cookies
|
||||
it.name.startsWith("__cf") || it.name.startsWith("cf_")
|
||||
}.map {
|
||||
// Convert cookies -> OkHttp format
|
||||
Cookie.Builder()
|
||||
.domain(it.domain.removePrefix("."))
|
||||
.expiresAt(it.expires?.time ?: Long.MAX_VALUE)
|
||||
.name(it.name)
|
||||
.path(it.path)
|
||||
.value(it.value).apply {
|
||||
if (it.isHttpOnly) httpOnly()
|
||||
if (it.isSecure) secure()
|
||||
}.build()
|
||||
companion object {
|
||||
private val ERROR_CODES = listOf(403, 503)
|
||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This class is ported from https://github.com/vvanglro/cf-clearance
|
||||
* The original code is licensed under Apache 2.0
|
||||
*/
|
||||
object CFClearance {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
|
||||
fun resolveWithWebView(originalRequest: Request): Request {
|
||||
val url = originalRequest.url.toString()
|
||||
|
||||
logger.debug { "resolveWithWebView($url)" }
|
||||
|
||||
val cookies = Playwright.create().use { playwright ->
|
||||
playwright.chromium().launch(
|
||||
LaunchOptions()
|
||||
.setHeadless(false)
|
||||
.apply {
|
||||
if (serverConfig.socksProxyEnabled) {
|
||||
setProxy("socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
|
||||
}
|
||||
}
|
||||
).use { browser ->
|
||||
val userAgent = originalRequest.header("User-Agent")
|
||||
if (userAgent != null) {
|
||||
browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
|
||||
browserContext.newPage().use { getCookies(it, url) }
|
||||
}
|
||||
} else {
|
||||
browser.newPage().use { getCookies(it, url) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy cookies to cookie store
|
||||
convertedCookies.forEach {
|
||||
cookies.groupBy { it.domain }.forEach { (domain, cookies) ->
|
||||
network.cookies.addAll(
|
||||
HttpUrl.Builder()
|
||||
url = HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(it.domain)
|
||||
.host(domain)
|
||||
.build(),
|
||||
listOf(it)
|
||||
cookies = cookies
|
||||
)
|
||||
}
|
||||
// Merge new and existing cookies for this request
|
||||
// Find the cookies that we need to merge into this request
|
||||
val convertedForThisRequest = convertedCookies.filter {
|
||||
it.matches(response.request.url)
|
||||
val convertedForThisRequest = cookies.filter {
|
||||
it.matches(originalRequest.url)
|
||||
}
|
||||
// Extract cookies from current request
|
||||
val existingCookies = Cookie.parseAll(
|
||||
response.request.url,
|
||||
response.request.headers
|
||||
originalRequest.url,
|
||||
originalRequest.headers
|
||||
)
|
||||
// Filter out existing values of cookies that we are about to merge in
|
||||
val filteredExisting = existingCookies.filter { existing ->
|
||||
convertedForThisRequest.none { converted -> converted.name == existing.name }
|
||||
}
|
||||
logger.trace { "Existing cookies" }
|
||||
logger.trace { existingCookies.joinToString("; ") }
|
||||
val newCookies = filteredExisting + convertedForThisRequest
|
||||
return response.request.newBuilder()
|
||||
.header("Cookie", newCookies.map { it.toString() }.joinToString("; "))
|
||||
logger.trace { "New cookies" }
|
||||
logger.trace { newCookies.joinToString("; ") }
|
||||
return originalRequest.newBuilder()
|
||||
.header("Cookie", newCookies.joinToString("; ") { "${it.name}=${it.value}" })
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||
private fun getCookies(page: Page, url: String): List<Cookie> {
|
||||
applyStealthInitScripts(page)
|
||||
page.navigate(url)
|
||||
val challengeResolved = waitForChallengeResolve(page)
|
||||
|
||||
return if (challengeResolved) {
|
||||
val cookies = page.context().cookies()
|
||||
|
||||
logger.debug {
|
||||
val userAgent = page.evaluate("() => {return navigator.userAgent}")
|
||||
"Playwright User-Agent is $userAgent"
|
||||
}
|
||||
|
||||
// Convert PlayWright cookies to OkHttp cookies
|
||||
cookies.map {
|
||||
Cookie.Builder()
|
||||
.domain(it.domain.removePrefix("."))
|
||||
.expiresAt(it.expires?.times(1000)?.toLong() ?: Long.MAX_VALUE)
|
||||
.name(it.name)
|
||||
.path(it.path)
|
||||
.value(it.value).apply {
|
||||
if (it.httpOnly) httpOnly()
|
||||
if (it.secure) secure()
|
||||
}.build()
|
||||
}
|
||||
} else {
|
||||
logger.debug { "Cloudflare challenge failed to resolve" }
|
||||
throw CloudflareBypassException()
|
||||
}
|
||||
}
|
||||
|
||||
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/stealth.py#L18
|
||||
private val stealthInitScripts by lazy {
|
||||
arrayOf(
|
||||
ServerConfig::class.java.getResource("/cloudflare-js/canvas.fingerprinting.js")!!.readText(),
|
||||
ServerConfig::class.java.getResource("/cloudflare-js/chrome.global.js")!!.readText(),
|
||||
ServerConfig::class.java.getResource("/cloudflare-js/emulate.touch.js")!!.readText(),
|
||||
ServerConfig::class.java.getResource("/cloudflare-js/navigator.permissions.js")!!.readText(),
|
||||
ServerConfig::class.java.getResource("/cloudflare-js/navigator.webdriver.js")!!.readText(),
|
||||
ServerConfig::class.java.getResource("/cloudflare-js/chrome.runtime.js")!!.readText(),
|
||||
ServerConfig::class.java.getResource("/cloudflare-js/chrome.plugin.js")!!.readText()
|
||||
)
|
||||
}
|
||||
|
||||
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/stealth.py#L76
|
||||
private fun applyStealthInitScripts(page: Page) {
|
||||
for (script in stealthInitScripts) {
|
||||
page.addInitScript(script)
|
||||
}
|
||||
}
|
||||
|
||||
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/retry.py#L21
|
||||
private fun waitForChallengeResolve(page: Page): Boolean {
|
||||
// sometimes the user has to solve the captcha challenge manually, potentially wait a long time
|
||||
val timeoutSeconds = 120
|
||||
repeat(timeoutSeconds) {
|
||||
page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS))
|
||||
val success = try {
|
||||
page.querySelector("#challenge-form") == null
|
||||
} catch (e: Exception) {
|
||||
logger.debug(e) { "query Error" }
|
||||
false
|
||||
}
|
||||
if (success) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private class CloudflareBypassException : Exception()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
(function () {
|
||||
const ORIGINAL_CANVAS = HTMLCanvasElement.prototype[name];
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, name, {
|
||||
"value": function () {
|
||||
var shift = {
|
||||
'r': Math.floor(Math.random() * 10) - 5,
|
||||
'g': Math.floor(Math.random() * 10) - 5,
|
||||
'b': Math.floor(Math.random() * 10) - 5,
|
||||
'a': Math.floor(Math.random() * 10) - 5
|
||||
};
|
||||
var width = this.width,
|
||||
height = this.height,
|
||||
context = this.getContext("2d");
|
||||
var imageData = context.getImageData(0, 0, width, height);
|
||||
for (var i = 0; i < height; i++) {
|
||||
for (var j = 0; j < width; j++) {
|
||||
var n = ((i * (width * 4)) + (j * 4));
|
||||
imageData.data[n + 0] = imageData.data[n + 0] + shift.r;
|
||||
imageData.data[n + 1] = imageData.data[n + 1] + shift.g;
|
||||
imageData.data[n + 2] = imageData.data[n + 2] + shift.b;
|
||||
imageData.data[n + 3] = imageData.data[n + 3] + shift.a;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
return ORIGINAL_CANVAS.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
})(this);
|
||||
52
server/src/main/resources/cloudflare-js/chrome.global.js
Normal file
52
server/src/main/resources/cloudflare-js/chrome.global.js
Normal file
@@ -0,0 +1,52 @@
|
||||
Object.defineProperty(window, 'chrome', {
|
||||
value: new Proxy(window.chrome, {
|
||||
has: (target, key) => true,
|
||||
get: (target, key) => {
|
||||
return {
|
||||
app: {
|
||||
isInstalled: false,
|
||||
},
|
||||
webstore: {
|
||||
onInstallStageChanged: {},
|
||||
onDownloadProgress: {},
|
||||
},
|
||||
runtime: {
|
||||
PlatformOs: {
|
||||
MAC: 'mac',
|
||||
WIN: 'win',
|
||||
ANDROID: 'android',
|
||||
CROS: 'cros',
|
||||
LINUX: 'linux',
|
||||
OPENBSD: 'openbsd',
|
||||
},
|
||||
PlatformArch: {
|
||||
ARM: 'arm',
|
||||
X86_32: 'x86-32',
|
||||
X86_64: 'x86-64',
|
||||
},
|
||||
PlatformNaclArch: {
|
||||
ARM: 'arm',
|
||||
X86_32: 'x86-32',
|
||||
X86_64: 'x86-64',
|
||||
},
|
||||
RequestUpdateCheckStatus: {
|
||||
THROTTLED: 'throttled',
|
||||
NO_UPDATE: 'no_update',
|
||||
UPDATE_AVAILABLE: 'update_available',
|
||||
},
|
||||
OnInstalledReason: {
|
||||
INSTALL: 'install',
|
||||
UPDATE: 'update',
|
||||
CHROME_UPDATE: 'chrome_update',
|
||||
SHARED_MODULE_UPDATE: 'shared_module_update',
|
||||
},
|
||||
OnRestartRequiredReason: {
|
||||
APP_UPDATE: 'app_update',
|
||||
OS_UPDATE: 'os_update',
|
||||
PERIODIC: 'periodic',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
203
server/src/main/resources/cloudflare-js/chrome.plugin.js
Normal file
203
server/src/main/resources/cloudflare-js/chrome.plugin.js
Normal file
@@ -0,0 +1,203 @@
|
||||
(function () {
|
||||
const plugin0 = Object.create(Plugin.prototype);
|
||||
|
||||
const mimeType0 = Object.create(MimeType.prototype);
|
||||
const mimeType1 = Object.create(MimeType.prototype);
|
||||
Object.defineProperties(mimeType0, {
|
||||
type: {
|
||||
get: () => 'application/pdf',
|
||||
},
|
||||
suffixes: {
|
||||
get: () => 'pdf',
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperties(mimeType1, {
|
||||
type: {
|
||||
get: () => 'text/pdf',
|
||||
},
|
||||
suffixes: {
|
||||
get: () => 'pdf',
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperties(plugin0, {
|
||||
name: {
|
||||
get: () => 'Chrome PDF Viewer',
|
||||
},
|
||||
description: {
|
||||
get: () => 'Portable Document Format',
|
||||
},
|
||||
0: {
|
||||
get: () => {
|
||||
return mimeType0;
|
||||
},
|
||||
},
|
||||
1: {
|
||||
get: () => {
|
||||
return mimeType1;
|
||||
},
|
||||
},
|
||||
length: {
|
||||
get: () => 2,
|
||||
},
|
||||
filename: {
|
||||
get: () => 'internal-pdf-viewer',
|
||||
},
|
||||
});
|
||||
|
||||
const plugin1 = Object.create(Plugin.prototype);
|
||||
Object.defineProperties(plugin1, {
|
||||
name: {
|
||||
get: () => 'Chromium PDF Viewer',
|
||||
},
|
||||
description: {
|
||||
get: () => 'Portable Document Format',
|
||||
},
|
||||
0: {
|
||||
get: () => {
|
||||
return mimeType0;
|
||||
},
|
||||
},
|
||||
1: {
|
||||
get: () => {
|
||||
return mimeType1;
|
||||
},
|
||||
},
|
||||
length: {
|
||||
get: () => 2,
|
||||
},
|
||||
filename: {
|
||||
get: () => 'internal-pdf-viewer',
|
||||
},
|
||||
});
|
||||
|
||||
const plugin2 = Object.create(Plugin.prototype);
|
||||
Object.defineProperties(plugin2, {
|
||||
name: {
|
||||
get: () => 'Microsoft Edge PDF Viewer',
|
||||
},
|
||||
description: {
|
||||
get: () => 'Portable Document Format',
|
||||
},
|
||||
0: {
|
||||
get: () => {
|
||||
return mimeType0;
|
||||
},
|
||||
},
|
||||
1: {
|
||||
get: () => {
|
||||
return mimeType1;
|
||||
},
|
||||
},
|
||||
length: {
|
||||
get: () => 2,
|
||||
},
|
||||
filename: {
|
||||
get: () => 'internal-pdf-viewer',
|
||||
},
|
||||
});
|
||||
|
||||
const plugin3 = Object.create(Plugin.prototype);
|
||||
Object.defineProperties(plugin3, {
|
||||
name: {
|
||||
get: () => 'PDF Viewer',
|
||||
},
|
||||
description: {
|
||||
get: () => 'Portable Document Format',
|
||||
},
|
||||
0: {
|
||||
get: () => {
|
||||
return mimeType0;
|
||||
},
|
||||
},
|
||||
1: {
|
||||
get: () => {
|
||||
return mimeType1;
|
||||
},
|
||||
},
|
||||
length: {
|
||||
get: () => 2,
|
||||
},
|
||||
filename: {
|
||||
get: () => 'internal-pdf-viewer',
|
||||
},
|
||||
});
|
||||
|
||||
const plugin4 = Object.create(Plugin.prototype);
|
||||
Object.defineProperties(plugin4, {
|
||||
name: {
|
||||
get: () => 'WebKit built-in PDF',
|
||||
},
|
||||
description: {
|
||||
get: () => 'Portable Document Format',
|
||||
},
|
||||
0: {
|
||||
get: () => {
|
||||
return mimeType0;
|
||||
},
|
||||
},
|
||||
1: {
|
||||
get: () => {
|
||||
return mimeType1;
|
||||
},
|
||||
},
|
||||
length: {
|
||||
get: () => 2,
|
||||
},
|
||||
filename: {
|
||||
get: () => 'internal-pdf-viewer',
|
||||
},
|
||||
});
|
||||
|
||||
const pluginArray = Object.create(PluginArray.prototype);
|
||||
|
||||
pluginArray['0'] = plugin0;
|
||||
pluginArray['1'] = plugin1;
|
||||
pluginArray['2'] = plugin2;
|
||||
pluginArray['3'] = plugin3;
|
||||
pluginArray['4'] = plugin4;
|
||||
|
||||
let refreshValue;
|
||||
|
||||
Object.defineProperties(pluginArray, {
|
||||
length: {
|
||||
get: () => 5,
|
||||
},
|
||||
item: {
|
||||
value: (index) => {
|
||||
if (index > 4294967295) {
|
||||
index = index % 4294967296;
|
||||
}
|
||||
switch (index) {
|
||||
case 0:
|
||||
return plugin3;
|
||||
case 1:
|
||||
return plugin0;
|
||||
case 2:
|
||||
return plugin1;
|
||||
case 3:
|
||||
return plugin2;
|
||||
case 4:
|
||||
return plugin4;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
refresh: {
|
||||
get: () => {
|
||||
return refreshValue;
|
||||
},
|
||||
set: (value) => {
|
||||
refreshValue = value;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Object.getPrototypeOf(navigator), 'plugins', {
|
||||
get: () => {
|
||||
return pluginArray;
|
||||
},
|
||||
});
|
||||
})();
|
||||
170
server/src/main/resources/cloudflare-js/chrome.runtime.js
Normal file
170
server/src/main/resources/cloudflare-js/chrome.runtime.js
Normal file
@@ -0,0 +1,170 @@
|
||||
(function () {
|
||||
window.chrome = {};
|
||||
window.chrome.app = {
|
||||
InstallState: {
|
||||
DISABLED: 'disabled',
|
||||
INSTALLED: 'installed',
|
||||
NOT_INSTALLED: 'not_installed',
|
||||
},
|
||||
RunningState: {
|
||||
CANNOT_RUN: 'cannot_run',
|
||||
READY_TO_RUN: 'ready_to_run',
|
||||
RUNNING: 'running',
|
||||
},
|
||||
getDetails: () => {
|
||||
'[native code]';
|
||||
},
|
||||
getIsInstalled: () => {
|
||||
'[native code]';
|
||||
},
|
||||
installState: () => {
|
||||
'[native code]';
|
||||
},
|
||||
get isInstalled() {
|
||||
return false;
|
||||
},
|
||||
runningState: () => {
|
||||
'[native code]';
|
||||
},
|
||||
};
|
||||
|
||||
window.chrome.runtime = {
|
||||
OnInstalledReason: {
|
||||
CHROME_UPDATE: 'chrome_update',
|
||||
INSTALL: 'install',
|
||||
SHARED_MODULE_UPDATE: 'shared_module_update',
|
||||
UPDATE: 'update',
|
||||
},
|
||||
OnRestartRequiredReason: {
|
||||
APP_UPDATE: 'app_update',
|
||||
OS_UPDATE: 'os_update',
|
||||
PERIODIC: 'periodic',
|
||||
},
|
||||
PlatformArch: {
|
||||
ARM: 'arm',
|
||||
ARM64: 'arm64',
|
||||
MIPS: 'mips',
|
||||
MIPS64: 'mips64',
|
||||
X86_32: 'x86-32',
|
||||
X86_64: 'x86-64',
|
||||
},
|
||||
PlatformNaclArch: {
|
||||
ARM: 'arm',
|
||||
MIPS: 'mips',
|
||||
MIPS64: 'mips64',
|
||||
X86_32: 'x86-32',
|
||||
X86_64: 'x86-64',
|
||||
},
|
||||
PlatformOs: {
|
||||
ANDROID: 'android',
|
||||
CROS: 'cros',
|
||||
FUCHSIA: 'fuchsia',
|
||||
LINUX: 'linux',
|
||||
MAC: 'mac',
|
||||
OPENBSD: 'openbsd',
|
||||
WIN: 'win',
|
||||
},
|
||||
RequestUpdateCheckStatus: {
|
||||
NO_UPDATE: 'no_update',
|
||||
THROTTLED: 'throttled',
|
||||
UPDATE_AVAILABLE: 'update_available',
|
||||
},
|
||||
connect() {
|
||||
'[native code]';
|
||||
},
|
||||
sendMessage() {
|
||||
'[native code]';
|
||||
},
|
||||
id: undefined,
|
||||
};
|
||||
|
||||
let startE = Date.now();
|
||||
window.chrome.csi = function () {
|
||||
'[native code]';
|
||||
return {
|
||||
startE: startE,
|
||||
onloadT: startE + 281,
|
||||
pageT: 3947.235,
|
||||
tran: 15,
|
||||
};
|
||||
};
|
||||
|
||||
window.chrome.loadTimes = function () {
|
||||
'[native code]';
|
||||
return {
|
||||
get requestTime() {
|
||||
return startE / 1000;
|
||||
},
|
||||
get startLoadTime() {
|
||||
return startE / 1000;
|
||||
},
|
||||
get commitLoadTime() {
|
||||
return startE / 1000 + 0.324;
|
||||
},
|
||||
get finishDocumentLoadTime() {
|
||||
return startE / 1000 + 0.498;
|
||||
},
|
||||
get finishLoadTime() {
|
||||
return startE / 1000 + 0.534;
|
||||
},
|
||||
get firstPaintTime() {
|
||||
return startE / 1000 + 0.437;
|
||||
},
|
||||
get firstPaintAfterLoadTime() {
|
||||
return 0;
|
||||
},
|
||||
get navigationType() {
|
||||
return 'Other';
|
||||
},
|
||||
get wasFetchedViaSpdy() {
|
||||
return true;
|
||||
},
|
||||
get wasNpnNegotiated() {
|
||||
return true;
|
||||
},
|
||||
get npnNegotiatedProtocol() {
|
||||
return 'h3';
|
||||
},
|
||||
get wasAlternateProtocolAvailable() {
|
||||
return false;
|
||||
},
|
||||
get connectionInfo() {
|
||||
return 'h3';
|
||||
},
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Bypass OOPIF test
|
||||
(function performance_memory() {
|
||||
const jsHeapSizeLimitInt = 4294705152;
|
||||
|
||||
const total_js_heap_size = 35244183;
|
||||
const used_js_heap_size = [
|
||||
17632315, 17632315, 17632315, 17634847, 17636091, 17636751,
|
||||
];
|
||||
|
||||
let counter = 0;
|
||||
|
||||
let MemoryInfoProto = Object.getPrototypeOf(performance.memory);
|
||||
Object.defineProperties(MemoryInfoProto, {
|
||||
jsHeapSizeLimit: {
|
||||
get: () => {
|
||||
return jsHeapSizeLimitInt;
|
||||
},
|
||||
},
|
||||
totalJSHeapSize: {
|
||||
get: () => {
|
||||
return total_js_heap_size;
|
||||
},
|
||||
},
|
||||
usedJSHeapSize: {
|
||||
get: () => {
|
||||
if (counter > 5) {
|
||||
counter = 0;
|
||||
}
|
||||
return used_js_heap_size[counter++];
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
3
server/src/main/resources/cloudflare-js/emulate.touch.js
Normal file
3
server/src/main/resources/cloudflare-js/emulate.touch.js
Normal file
@@ -0,0 +1,3 @@
|
||||
Object.defineProperty(navigator, 'maxTouchPoints', {
|
||||
get: () => 1
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
// https://github.com/microlinkhq/browserless/blob/master/packages/goto/src/evasions/navigator-permissions.js
|
||||
if (!window.Notification) {
|
||||
window.Notification = {
|
||||
permission: 'denied'
|
||||
}
|
||||
}
|
||||
const originalQuery = window.navigator.permissions.query
|
||||
window.navigator.permissions.__proto__.query = parameters =>
|
||||
parameters.name === 'notifications'
|
||||
? Promise.resolve({state: window.Notification.permission})
|
||||
: originalQuery(parameters)
|
||||
const oldCall = Function.prototype.call
|
||||
|
||||
function call() {
|
||||
return oldCall.apply(this, arguments)
|
||||
}
|
||||
|
||||
Function.prototype.call = call
|
||||
const nativeToStringFunctionString = Error.toString().replace(/Error/g, 'toString')
|
||||
const oldToString = Function.prototype.toString
|
||||
|
||||
function functionToString() {
|
||||
if (this === window.navigator.permissions.query) {
|
||||
return 'function query() { [native code] }'
|
||||
}
|
||||
if (this === functionToString) {
|
||||
return nativeToStringFunctionString
|
||||
}
|
||||
return oldCall.call(oldToString, this)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
Function.prototype.toString = functionToString
|
||||
@@ -0,0 +1,5 @@
|
||||
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
||||
get() {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
58
server/src/test/kotlin/masstest/CloudFlareTest.kt
Normal file
58
server/src/test/kotlin/masstest/CloudFlareTest.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
package masstest
|
||||
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import mu.KotlinLogging
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.test.BASE_PATH
|
||||
import suwayomi.tachidesk.test.setLoggingEnabled
|
||||
import xyz.nulldev.ts.config.CONFIG_PREFIX
|
||||
import java.io.File
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class CloudFlareTest {
|
||||
lateinit var nhentai: HttpSource
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
val dataRoot = File(BASE_PATH).absolutePath
|
||||
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
|
||||
applicationSetup()
|
||||
setLoggingEnabled(false)
|
||||
|
||||
runBlocking {
|
||||
val extensions = ExtensionsList.getExtensionList()
|
||||
with(extensions.first { it.name == "NHentai" }) {
|
||||
if (!installed) {
|
||||
Extension.installExtension(pkgName)
|
||||
} else if (hasUpdate) {
|
||||
Extension.updateExtension(pkgName)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
nhentai = Source.getSourceList()
|
||||
.firstNotNullOf { it.id.toLong().takeIf { it == 3122156392225024195L } }
|
||||
.let(GetCatalogueSource::getCatalogueSourceOrNull) as HttpSource
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
}
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Test
|
||||
fun `test nhentai browse`() = runTest {
|
||||
assert(nhentai.fetchPopularManga(1).awaitSingle().mangas.isNotEmpty()) {
|
||||
"NHentai results were empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user