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
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
val tachideskRevision = runCatching {

View File

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

View File

@@ -51,7 +51,7 @@ dependencies {
implementation(libs.junrar)
// CloudflareInterceptor
implementation(libs.playwright)
implementation(libs.jep)
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
implementation(libs.bouncycastle)
@@ -168,7 +168,8 @@ tasks {
application.applicationDefaultJvmArgs = listOf(
"-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// 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
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.interceptor.CFClearance.resolveWithWebView
import jep.SharedInterpreter
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import mu.KotlinLogging
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
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 uy.kohesive.injekt.injectLazy
import java.io.IOException
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import java.util.concurrent.TimeUnit
class CloudflareInterceptor : Interceptor {
private val logger = KotlinLogging.logger {}
@@ -38,10 +38,11 @@ class CloudflareInterceptor : Interceptor {
return originalResponse
}
throw IOException("playwrite is diabled for v0.6.7")
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 {
originalResponse.close()
network.cookies.remove(originalRequest.url.toUri())
@@ -72,9 +73,14 @@ object CFClearance {
private val network: NetworkHelper by injectLazy()
init {
// Fix the default DriverJar issue by providing our own implementation
// ref: https://github.com/microsoft/playwright-java/issues/1138
System.setProperty("playwright.driver.impl", "suwayomi.tachidesk.server.util.DriverJar")
SharedInterpreter().use { jep ->
val uc = "/home/armor/programming/github-clones/undetected-chromedriver"
jep.exec("import sys")
jep.exec("sys.path.insert(0,'$uc')")
jep.exec("import undetected_chromedriver") // Cache import
}
}
fun resolveWithWebView(originalRequest: Request): Request {
@@ -82,24 +88,31 @@ object CFClearance {
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) }
val cookies = SharedInterpreter().use { jep ->
try {
jep.exec("import undetected_chromedriver as uc")
jep.exec("options = uc.ChromeOptions()")
if (serverConfig.socksProxyEnabled) {
val proxy = "socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}"
jep.exec("options.add_argument('--proxy-server=$proxy')")
}
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 {
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 ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(true)
).use { browser ->
browser.newPage().use { page ->
val userAgent = page.evaluate("() => {return navigator.userAgent}") as String
logger.debug { "WebView User-Agent is $userAgent" }
return userAgent
}
}
SharedInterpreter().use { jep ->
jep.exec("import undetected_chromedriver as uc")
jep.exec("options = uc.ChromeOptions()")
jep.exec("options.add_argument('--headless')")
jep.exec("options.add_argument('--disable-gpu')")
jep.exec("driver = uc.Chrome(options=options)")
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) {
// Playwright 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"
} catch (e: Exception) {
// Webview might fail on headless environments like docker
"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> {
applyStealthInitScripts(page)
page.navigate(url)
val challengeResolved = waitForChallengeResolve(page)
@Serializable
data class PythonSeleniumCookie(
val domain: String,
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) {
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 {
val userAgent = page.evaluate("() => {return navigator.userAgent}")
"Playwright User-Agent is $userAgent"
jep.exec("userAgent = driver.execute_script('return navigator.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 {
Cookie.Builder()
.domain(it.domain.removePrefix("."))
.expiresAt(it.expires?.times(1000)?.toLong() ?: Long.MAX_VALUE)
.expiresAt(it.expiry?.times(1000) ?: Long.MAX_VALUE)
.name(it.name)
.path(it.path)
.value(it.value).apply {
@@ -185,39 +216,18 @@ object CFClearance {
}.build()
}
} else {
logger.debug { "Cloudflare challenge failed to resolve" }
throw CloudflareBypassException()
throw CloudflareBypassException("Cloudflare challenge failed to resolve")
}
}
// 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
private fun waitForChallengeResolve(jep: SharedInterpreter): Boolean {
// sometimes the user has to solve the captcha challenge manually and multiple times, potentially wait a long time
val timeoutSeconds = 120
repeat(timeoutSeconds) {
page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS))
TimeUnit.SECONDS.sleep(1)
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) {
logger.debug(e) { "query Error" }
false
@@ -226,6 +236,6 @@ object CFClearance {
}
return false
}
private class CloudflareBypassException : Exception()
}
private class CloudflareBypassException(message: String?) : Exception(message)

View File

@@ -1,5 +1,12 @@
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 io.javalin.http.HttpCode
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.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 {
private val logger = KotlinLogging.logger { }
@@ -115,7 +115,7 @@ object UpdateController {
updater.addMangasToQueue(
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
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by overridableConfig
val webviewEnabled : Boolean by overridableConfig
companion object {
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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import kotlinx.serialization.json.Json
import mu.KotlinLogging
import net.lingala.zip4j.ZipFile
import org.kodein.di.DI
@@ -16,7 +15,6 @@ import org.kodein.di.instance
import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
import java.net.HttpURLConnection
@@ -26,7 +24,6 @@ import java.security.MessageDigest
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val json: Json by injectLazy()
private val tmpDir = System.getProperty("java.io.tmpdir")
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
server.debugLogsEnabled = false
server.systemTrayEnabled = true
server.webviewEnabled = false