migrate webview solution to Jep

This commit is contained in:
Aria Moradi
2023-04-24 18:26:04 +03:30
parent cde5dc5bfa
commit cbefe1125d
15 changed files with 109 additions and 594 deletions

View File

@@ -12,7 +12,7 @@ const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.7.0" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.7.0"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r983" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r1045"
// counts commits on the master branch // counts commits on the master branch
val tachideskRevision = runCatching { val tachideskRevision = runCatching {

View File

@@ -10,7 +10,6 @@ dex2jar = "v60"
rhino = "1.7.14" rhino = "1.7.14"
settings = "1.0.0-RC" settings = "1.0.0-RC"
twelvemonkeys = "3.9.4" twelvemonkeys = "3.9.4"
playwright = "1.28.0"
[libraries] [libraries]
# Kotlin # Kotlin
@@ -95,8 +94,8 @@ appdirs = "net.harawata:appdirs:1.2.1"
zip4j = "net.lingala.zip4j:zip4j:2.11.2" zip4j = "net.lingala.zip4j:zip4j:2.11.2"
junrar = "com.github.junrar:junrar:7.5.3" junrar = "com.github.junrar:junrar:7.5.3"
# CloudflareInterceptor # CloudflareInterceptor WebView
playwright = { module = "com.microsoft.playwright:playwright", version.ref = "playwright" } jep = "black.ninia:jep:4.1.1"
# AES/CBC/PKCS7Padding Cypher provider # AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.72" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.72"

View File

@@ -51,7 +51,7 @@ dependencies {
implementation(libs.junrar) implementation(libs.junrar)
// CloudflareInterceptor // CloudflareInterceptor
implementation(libs.playwright) implementation(libs.jep)
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga // AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
implementation(libs.bouncycastle) implementation(libs.bouncycastle)
@@ -168,7 +168,8 @@ tasks {
application.applicationDefaultJvmArgs = listOf( application.applicationDefaultJvmArgs = listOf(
"-Dsuwayomi.tachidesk.config.server.webUIInterface=electron", "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// Change this to the installed electron application // Change this to the installed electron application
"-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron" "-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron",
// "-Djava.library.path=/home/armor/programming/github-clones/undetected-chromedriver/venv/lib/python3.10/site-packages/jep"
) )
} }
} }

View File

@@ -1,24 +1,24 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import com.microsoft.playwright.Browser
import com.microsoft.playwright.BrowserType.LaunchOptions
import com.microsoft.playwright.Page
import com.microsoft.playwright.Playwright
import com.microsoft.playwright.PlaywrightException
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.interceptor.CFClearance.resolveWithWebView import eu.kanade.tachiyomi.network.interceptor.CFClearance.resolveWithWebView
import jep.SharedInterpreter
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import suwayomi.tachidesk.server.ServerConfig import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import kotlin.time.Duration.Companion.seconds import java.util.concurrent.TimeUnit
import kotlin.time.DurationUnit
class CloudflareInterceptor : Interceptor { class CloudflareInterceptor : Interceptor {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -38,10 +38,11 @@ class CloudflareInterceptor : Interceptor {
return originalResponse return originalResponse
} }
throw IOException("playwrite is diabled for v0.6.7")
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." } logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
if (!serverConfig.webviewEnabled)
throw CloudflareBypassException("Webview is disabled, enable it in server config")
return try { return try {
originalResponse.close() originalResponse.close()
network.cookies.remove(originalRequest.url.toUri()) network.cookies.remove(originalRequest.url.toUri())
@@ -72,9 +73,14 @@ object CFClearance {
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
init { init {
// Fix the default DriverJar issue by providing our own implementation SharedInterpreter().use { jep ->
// ref: https://github.com/microsoft/playwright-java/issues/1138 val uc = "/home/armor/programming/github-clones/undetected-chromedriver"
System.setProperty("playwright.driver.impl", "suwayomi.tachidesk.server.util.DriverJar")
jep.exec("import sys")
jep.exec("sys.path.insert(0,'$uc')")
jep.exec("import undetected_chromedriver") // Cache import
}
} }
fun resolveWithWebView(originalRequest: Request): Request { fun resolveWithWebView(originalRequest: Request): Request {
@@ -82,24 +88,31 @@ object CFClearance {
logger.debug { "resolveWithWebView($url)" } logger.debug { "resolveWithWebView($url)" }
val cookies = Playwright.create().use { playwright -> val cookies = SharedInterpreter().use { jep ->
playwright.chromium().launch( try {
LaunchOptions() jep.exec("import undetected_chromedriver as uc")
.setHeadless(false)
.apply { jep.exec("options = uc.ChromeOptions()")
if (serverConfig.socksProxyEnabled) {
setProxy("socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") if (serverConfig.socksProxyEnabled) {
} val proxy = "socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}"
} jep.exec("options.add_argument('--proxy-server=$proxy')")
).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) }
} }
jep.exec("driver = uc.Chrome(options=options)")
// 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) }
// }
jep.exec("driver.get('$url')")
getCookies(jep, url)
} finally {
jep.exec("driver.quit()")
} }
} }
@@ -139,44 +152,62 @@ object CFClearance {
fun getWebViewUserAgent(): String { fun getWebViewUserAgent(): String {
return try { return try {
throw PlaywrightException("playwrite is diabled for v0.6.7") if (!serverConfig.webviewEnabled)
throw CloudflareBypassException("Webview is disabled, enable it in server config")
Playwright.create().use { playwright -> SharedInterpreter().use { jep ->
playwright.chromium().launch( jep.exec("import undetected_chromedriver as uc")
LaunchOptions()
.setHeadless(true) jep.exec("options = uc.ChromeOptions()")
).use { browser -> jep.exec("options.add_argument('--headless')")
browser.newPage().use { page -> jep.exec("options.add_argument('--disable-gpu')")
val userAgent = page.evaluate("() => {return navigator.userAgent}") as String jep.exec("driver = uc.Chrome(options=options)")
logger.debug { "WebView User-Agent is $userAgent" }
return userAgent jep.exec("userAgent = driver.execute_script('return navigator.userAgent')")
} val userAgent = jep.getValue("userAgent", java.lang.String::class.java).toString()
} jep.exec("driver.quit()")
userAgent
} }
} catch (e: PlaywrightException) { } catch (e: Exception) {
// Playwright might fail on headless environments like docker // Webview might fail on headless environments like docker
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
} }
} }
private fun getCookies(page: Page, url: String): List<Cookie> { @Serializable
applyStealthInitScripts(page) data class PythonSeleniumCookie(
page.navigate(url) val domain: String,
val challengeResolved = waitForChallengeResolve(page) val expiry: Long?,
val httpOnly: Boolean,
val name: String,
val path: String,
val sameSite: String,
val secure: Boolean,
val value: String
)
private val json by DI.global.instance<Json>()
private fun getCookies(jep: SharedInterpreter, url: String): List<Cookie> {
val challengeResolved = waitForChallengeResolve(jep)
return if (challengeResolved) { return if (challengeResolved) {
val cookies = page.context().cookies() jep.exec("import json")
jep.exec("cookies = json.dumps(driver.get_cookies())")
val cookiesJson = jep.getValue("cookies", java.lang.String::class.java).toString()
val cookies = json.decodeFromString<List<PythonSeleniumCookie>>(cookiesJson)
logger.debug { logger.debug {
val userAgent = page.evaluate("() => {return navigator.userAgent}") jep.exec("userAgent = driver.execute_script('return navigator.userAgent')")
"Playwright User-Agent is $userAgent" val userAgent = jep.getValue("userAgent", java.lang.String::class.java).toString()
"Webview User-Agent is $userAgent"
} }
// Convert PlayWright cookies to OkHttp cookies // Convert Webview cookies to OkHttp cookies
cookies.map { cookies.map {
Cookie.Builder() Cookie.Builder()
.domain(it.domain.removePrefix(".")) .domain(it.domain.removePrefix("."))
.expiresAt(it.expires?.times(1000)?.toLong() ?: Long.MAX_VALUE) .expiresAt(it.expiry?.times(1000) ?: Long.MAX_VALUE)
.name(it.name) .name(it.name)
.path(it.path) .path(it.path)
.value(it.value).apply { .value(it.value).apply {
@@ -185,39 +216,18 @@ object CFClearance {
}.build() }.build()
} }
} else { } else {
logger.debug { "Cloudflare challenge failed to resolve" } throw CloudflareBypassException("Cloudflare challenge failed to resolve")
throw CloudflareBypassException()
} }
} }
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/stealth.py#L18 private fun waitForChallengeResolve(jep: SharedInterpreter): Boolean {
private val stealthInitScripts by lazy { // sometimes the user has to solve the captcha challenge manually and multiple times, potentially wait a long time
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 val timeoutSeconds = 120
repeat(timeoutSeconds) { repeat(timeoutSeconds) {
page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS)) TimeUnit.SECONDS.sleep(1)
val success = try { val success = try {
page.querySelector("#challenge-form") == null jep.exec("r = driver.execute_script('return document.querySelector(\"#challenge-form\") == null')")
jep.getValue("r", java.lang.Boolean::class.java).toString().toBoolean()
} catch (e: Exception) { } catch (e: Exception) {
logger.debug(e) { "query Error" } logger.debug(e) { "query Error" }
false false
@@ -226,6 +236,6 @@ object CFClearance {
} }
return false return false
} }
private class CloudflareBypassException : Exception()
} }
private class CloudflareBypassException(message: String?) : Exception(message)

View File

@@ -1,5 +1,12 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import io.javalin.http.HttpCode import io.javalin.http.HttpCode
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
@@ -20,13 +27,6 @@ import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
object UpdateController { object UpdateController {
private val logger = KotlinLogging.logger { } private val logger = KotlinLogging.logger { }
@@ -115,7 +115,7 @@ object UpdateController {
updater.addMangasToQueue( updater.addMangasToQueue(
mangasToUpdate mangasToUpdate
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)), .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title))
) )
} }

View File

@@ -44,6 +44,7 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro
// misc // misc
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by overridableConfig val systemTrayEnabled: Boolean by overridableConfig
val webviewEnabled : Boolean by overridableConfig
companion object { companion object {
fun register(config: Config) = ServerConfig(config.getConfig(MODULE_NAME)) fun register(config: Config) = ServerConfig(config.getConfig(MODULE_NAME))

View File

@@ -7,7 +7,6 @@ package suwayomi.tachidesk.server.util
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import kotlinx.serialization.json.Json
import mu.KotlinLogging import mu.KotlinLogging
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import org.kodein.di.DI import org.kodein.di.DI
@@ -16,7 +15,6 @@ import org.kodein.di.instance
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -26,7 +24,6 @@ import java.security.MessageDigest
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val json: Json by injectLazy()
private val tmpDir = System.getProperty("java.io.tmpdir") private val tmpDir = System.getProperty("java.io.tmpdir")
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }

View File

@@ -1,28 +0,0 @@
(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);

View File

@@ -1,52 +0,0 @@
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',
},
},
}
}
})
});

View File

@@ -1,203 +0,0 @@
(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;
},
});
})();

View File

@@ -1,170 +0,0 @@
(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++];
},
},
});
})();

View File

@@ -1,3 +0,0 @@
Object.defineProperty(navigator, 'maxTouchPoints', {
get: () => 1
});

View File

@@ -1,33 +0,0 @@
// 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

View File

@@ -1,5 +0,0 @@
Object.defineProperty(Navigator.prototype, 'webdriver', {
get() {
return false;
},
});

View File

@@ -29,3 +29,4 @@ server.basicAuthPassword = ""
# misc # misc
server.debugLogsEnabled = false server.debugLogsEnabled = false
server.systemTrayEnabled = true server.systemTrayEnabled = true
server.webviewEnabled = false