mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-08 13:24:34 -05:00
Browser Webview (#1486)
* WebView: Add initial controller Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> * WebView: Prepare page * WebView: Basic HTML setup * WebView: Improve navigation * WebView: Refactor message class deserialization * WebView: Refactor event message serialization * WebView: Handle click events * WebView: Fix events after refactor * WebView: Fix normalizing of URLs * WebView: HTML remove navigation buttons * WebView: Handle more events * WebView: Handle document change in events * WebView: Refactor to send mutation events * WebView: More mouse events * WebView: Include bubbles, cancelable in event Those seem to be important * WebView: Attempt to support nested iframe * WebView: Handle long titles * WebView: Avoid setting invalid url * WebView: Send mousemove * WebView: Start switch to canvas-based render * WebView: Send on every render * WebView: Dynamic size * WebView: Keyboard events * WebView: Handle mouse events in CEF This is important because JS can't click into iFrames, meaning the previous solution doesn't work for captchas * WebView: Cleanup * WebView: Cleanup 2 * WebView: Document title * WebView: Also send title on address change * WebView: Load and flush cookies from store * WebView: remove outdated TODOs * Offline WebView: Load cookies from store * Cleanup * Add KcefCookieManager, need to figure out how to inject it * ktLintFormat * Fix a few cookie bugs * Fix Webview on Windows * Minor cleanup * WebView: Remove /tmp image write, lint * Remove custom cookie manager * Multiple cookie fixes * Minor fix * Minor cleanup and add support for MacOS meta key * Get enter working * WebView HTML: Make responsive for mobile pages * WebView: Translate touch events to mouse scroll * WebView: Overlay an actual input to allow typing on mobile Browsers will only show the keyboard if an input is focused. This also removes the `tabstop` hack. * WebView: Protect against occasional NullPointerException * WebView: Use float for clientX/Y * WebView: Fix ChromeAndroid being a pain * Simplify enter fix * NetworkHelper: Fix cache * Improve CookieStore url matching, fix another cookie conversion issue * Move distinctBy * WebView: Mouse direction toggle * Remove accidentally copied comment --------- Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
This commit is contained in:
@@ -25,10 +25,10 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.brotli.BrotliInterceptor
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import java.io.File
|
||||
import java.net.CookieHandler
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NetworkHelper(
|
||||
@@ -79,7 +79,7 @@ class NetworkHelper(
|
||||
.callTimeout(2, TimeUnit.MINUTES)
|
||||
.cache(
|
||||
Cache(
|
||||
directory = File.createTempFile("tachidesk_network_cache", null),
|
||||
directory = Files.createTempDirectory("tachidesk_network_cache").toFile(),
|
||||
maxSize = 5L * 1024 * 1024, // 5 MiB
|
||||
),
|
||||
).addInterceptor(UncaughtExceptionInterceptor())
|
||||
|
||||
@@ -8,8 +8,6 @@ import okio.withLock
|
||||
import java.net.CookieStore
|
||||
import java.net.HttpCookie
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -18,30 +16,40 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class PersistentCookieStore(
|
||||
context: Context,
|
||||
) : CookieStore {
|
||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||
private val cookieMap = mutableMapOf<String, List<Cookie>>()
|
||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
init {
|
||||
val domains =
|
||||
prefs.all.keys
|
||||
.map { it.substringBeforeLast(".") }
|
||||
.toSet()
|
||||
domains.forEach { domain ->
|
||||
val cookies = prefs.getStringSet(domain, emptySet())
|
||||
if (!cookies.isNullOrEmpty()) {
|
||||
try {
|
||||
val url = "http://$domain".toHttpUrlOrNull() ?: return@forEach
|
||||
val nonExpiredCookies =
|
||||
cookies
|
||||
.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap[domain] = nonExpiredCookies
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
lock.withLock {
|
||||
val domains =
|
||||
prefs.all.keys
|
||||
.map { it.substringBeforeLast(".") }
|
||||
.toSet()
|
||||
val domainsToSave = mutableSetOf<String>()
|
||||
domains.forEach { domain ->
|
||||
val cookies = prefs.getStringSet(domain, emptySet())
|
||||
if (!cookies.isNullOrEmpty()) {
|
||||
try {
|
||||
val url = "http://$domain".toHttpUrlOrNull() ?: return@forEach
|
||||
val nonExpiredCookies =
|
||||
cookies
|
||||
.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
.groupBy { it.domain }
|
||||
.mapValues { it.value.distinctBy { it.name } }
|
||||
nonExpiredCookies.forEach { (domain, cookies) ->
|
||||
cookieMap[domain] = cookies
|
||||
domainsToSave.add(domain)
|
||||
}
|
||||
domainsToSave.add(domain)
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
saveToDisk(domainsToSave)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +58,10 @@ class PersistentCookieStore(
|
||||
cookies: List<Cookie>,
|
||||
) {
|
||||
lock.withLock {
|
||||
val domainsToSave = mutableSetOf<String>()
|
||||
// Append or replace the cookies for this domain.
|
||||
val cookiesForDomain = cookieMap[url.host].orEmpty().toMutableList()
|
||||
for (cookie in cookies) {
|
||||
val cookiesForDomain = cookieMap[cookie.domain].orEmpty().toMutableList()
|
||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
|
||||
if (pos == -1) {
|
||||
@@ -60,10 +69,11 @@ class PersistentCookieStore(
|
||||
} else {
|
||||
cookiesForDomain[pos] = cookie
|
||||
}
|
||||
cookieMap[cookie.domain] = cookiesForDomain
|
||||
domainsToSave.add(cookie.domain)
|
||||
}
|
||||
cookieMap[url.host] = cookiesForDomain
|
||||
|
||||
saveToDisk(url.toUrl())
|
||||
saveToDisk(domainsToSave.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,48 +95,66 @@ class PersistentCookieStore(
|
||||
|
||||
override fun get(uri: URI): List<HttpCookie> {
|
||||
val url = uri.toURL()
|
||||
return get(url.host).map {
|
||||
return get(url.toHttpUrlOrNull()!!).map {
|
||||
it.toHttpCookie()
|
||||
}
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl): List<Cookie> = get(url.host)
|
||||
fun get(url: HttpUrl): List<Cookie> =
|
||||
lock.withLock {
|
||||
cookieMap.entries
|
||||
.filter {
|
||||
url.host.endsWith(it.key)
|
||||
}.flatMap { it.value }
|
||||
}
|
||||
|
||||
override fun add(
|
||||
uri: URI?,
|
||||
cookie: HttpCookie,
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
|
||||
val url = uri.toURL()
|
||||
lock.withLock {
|
||||
val cookies = cookieMap[url.host]
|
||||
cookieMap[url.host] = cookies.orEmpty() + cookie.toCookie(uri)
|
||||
saveToDisk(url)
|
||||
val cookie = cookie.toCookie()
|
||||
val cookiesForDomain = cookieMap[cookie.domain].orEmpty().toMutableList()
|
||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
|
||||
if (pos == -1) {
|
||||
cookiesForDomain.add(cookie)
|
||||
} else {
|
||||
cookiesForDomain[pos] = cookie
|
||||
}
|
||||
cookieMap[cookie.domain] = cookiesForDomain
|
||||
saveToDisk(setOf(cookie.domain))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCookies(): List<HttpCookie> =
|
||||
cookieMap.values.flatMap {
|
||||
it.map {
|
||||
it.toHttpCookie()
|
||||
lock.withLock {
|
||||
cookieMap.values.flatMap {
|
||||
it.map {
|
||||
it.toHttpCookie()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getStoredCookies(): List<Cookie> =
|
||||
lock.withLock {
|
||||
cookieMap.values.flatMap { it }
|
||||
}
|
||||
|
||||
override fun getURIs(): List<URI> =
|
||||
cookieMap.keys().toList().map {
|
||||
URI("http://$it")
|
||||
lock.withLock {
|
||||
cookieMap.keys.toList().map {
|
||||
URI("http://$it")
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(
|
||||
uri: URI?,
|
||||
cookie: HttpCookie,
|
||||
): Boolean {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
|
||||
val url = uri.toURL()
|
||||
return lock.withLock {
|
||||
val cookies = cookieMap[url.host].orEmpty()
|
||||
): Boolean =
|
||||
lock.withLock {
|
||||
val cookie = cookie.toCookie()
|
||||
val cookies = cookieMap[cookie.domain].orEmpty()
|
||||
val index =
|
||||
cookies.indexOfFirst {
|
||||
it.name == cookie.name &&
|
||||
@@ -135,63 +163,73 @@ class PersistentCookieStore(
|
||||
if (index >= 0) {
|
||||
val newList = cookies.toMutableList()
|
||||
newList.removeAt(index)
|
||||
cookieMap[url.host] = newList.toList()
|
||||
saveToDisk(url)
|
||||
cookieMap[cookie.domain] = newList.toList()
|
||||
saveToDisk(setOf(cookie.domain))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun get(url: String): List<Cookie> = cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
|
||||
private fun saveToDisk(url: URL) {
|
||||
private fun saveToDisk(domains: Set<String>) {
|
||||
// Get cookies to be stored in disk
|
||||
val newValues =
|
||||
cookieMap[url.host]
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.filter { it.persistent && !it.hasExpired() }
|
||||
.map(Cookie::toString)
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(url.host, newValues).apply()
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
domains.forEach { domain ->
|
||||
val newValues =
|
||||
cookieMap[domain]
|
||||
.orEmpty()
|
||||
.onEach { println(it) }
|
||||
.asSequence()
|
||||
.filter { it.persistent && !it.hasExpired() }
|
||||
.map(Cookie::toString)
|
||||
.toSet()
|
||||
if (newValues.isNotEmpty()) {
|
||||
remove(domain)
|
||||
putStringSet(domain, newValues)
|
||||
} else {
|
||||
remove(domain)
|
||||
}
|
||||
}
|
||||
}.apply()
|
||||
}
|
||||
|
||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
|
||||
|
||||
private fun HttpCookie.toCookie(uri: URI) =
|
||||
private fun HttpCookie.toCookie() =
|
||||
Cookie
|
||||
.Builder()
|
||||
.name(name)
|
||||
.value(value)
|
||||
.domain(uri.toURL().host)
|
||||
.domain(domain.removePrefix("."))
|
||||
.path(path ?: "/")
|
||||
.let {
|
||||
.also {
|
||||
if (maxAge != -1L) {
|
||||
it.expiresAt(System.currentTimeMillis() + maxAge.seconds.inWholeMilliseconds)
|
||||
} else {
|
||||
it.expiresAt(Long.MAX_VALUE)
|
||||
}
|
||||
}.let {
|
||||
if (secure) {
|
||||
it.secure()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.let {
|
||||
if (isHttpOnly) {
|
||||
it.httpOnly()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
if (!domain.startsWith('.')) {
|
||||
it.hostOnlyDomain(domain.removePrefix("."))
|
||||
}
|
||||
}.build()
|
||||
|
||||
private fun Cookie.toHttpCookie(): HttpCookie {
|
||||
val it = this
|
||||
return HttpCookie(it.name, it.value).apply {
|
||||
domain = it.domain
|
||||
domain =
|
||||
if (hostOnly) {
|
||||
it.domain
|
||||
} else {
|
||||
"." + it.domain
|
||||
}
|
||||
path = it.path
|
||||
secure = it.secure
|
||||
maxAge =
|
||||
|
||||
@@ -238,6 +238,7 @@ object CFClearance {
|
||||
if (!cookie.path.isNullOrEmpty()) it.path(cookie.path)
|
||||
// We need to convert the expires time to milliseconds for the persistent cookie store
|
||||
if (cookie.expires != null && cookie.expires > 0) it.expiresAt((cookie.expires * 1000).toLong())
|
||||
if (!cookie.domain.startsWith('.')) it.hostOnlyDomain(cookie.domain.removePrefix("."))
|
||||
}.build()
|
||||
}.groupBy { it.domain }
|
||||
.flatMap { (domain, cookies) ->
|
||||
|
||||
Reference in New Issue
Block a user