Migrate cookies store to CookieManager backend

This commit is contained in:
tarsin committed 2023-06-08 16:11:48 +08:00
1 parent ad04681460
commit 55b3753e66
10 files changed
+81 -389

No files matched your search

@@ -37,6 +37,7 @@ import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.legacy.cleanObsoleteCache
import com.hippo.ehviewer.legacy.migrateCookies
import com.hippo.ehviewer.okhttp.cache
import com.hippo.ehviewer.okhttp.httpClient
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
@@ -76,6 +77,9 @@ class EhApplication : Application(), ImageLoaderFactory {
}
}
launchIO {
launchIO {
migrateCookies()
}
launchIO {
EhTagDatabase.update()
}
@@ -15,45 +15,42 @@
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.network.CookieDatabase
import com.hippo.ehviewer.network.CookieSet
import eu.kanade.tachiyomi.util.lang.launchIO
import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import splitties.init.appCtx
import java.util.Collections
import java.util.regex.Pattern
object EhCookieStore : CookieJar {
private val db: CookieDatabase = CookieDatabase(appCtx)
private val map: MutableMap<String, CookieSet> = db.allCookies
private val manager = CookieManager.getInstance()
fun signOut() = manager.removeAllCookies(null)
fun contains(url: HttpUrl, name: String) = get(url).any { it.name == name }
fun signOut() {
clear()
fun get(url: HttpUrl): List<Cookie> {
val cookies = manager.getCookie(url.toString())
return if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }.filterNot { it.name == KEY_UTMP_NAME }
} else {
emptyList()
}
}
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPB_MEMBER_ID) &&
contains(url, KEY_IPB_PASS_HASH)
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
}
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
const val KEY_SETTINGS_PROFILE = "sp"
const val KEY_STAR = "star"
private const val KEY_STAR = "star"
private const val KEY_CONTENT_WARNING = "nw"
private const val CONTENT_WARNING_NOT_SHOW = "1"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(KEY_CONTENT_WARNING)
.value(CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build()
private const val KEY_UTMP_NAME = "__utmp"
private val sTipsCookie: Cookie = Cookie.Builder().name(KEY_CONTENT_WARNING).value(CONTENT_WARNING_NOT_SHOW).domain(EhUrl.DOMAIN_E).path("/").expiresAt(Long.MAX_VALUE).build()
fun newCookie(
cookie: Cookie,
@@ -87,64 +84,22 @@ object EhCookieStore : CookieJar {
return builder.build()
}
fun copyCookie(domain: String, newDomain: String, name: String, path: String = "/") {
val cookie = map[domain]?.get(name, domain, path)
cookie?.let { addCookie(newCookie(it, newDomain)) }
fun copyNecessaryCookies() {
val cookie = get(EhUrl.HOST_E.toHttpUrl()).filter { it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH || it.name == KEY_IGNEOUS }
cookie.forEach { manager.setCookie(EhUrl.HOST_EX, it.toString()) }
}
fun deleteCookie(url: HttpUrl, name: String) {
val deletedCookie = Cookie.Builder()
.name(name)
.value("deleted")
.domain(url.host)
.expiresAt(0)
.build()
addCookie(deletedCookie)
val deletedCookie = Cookie.Builder().name(name).value("deleted").domain(url.host).expiresAt(0).build()
manager.setCookie(url.toString(), deletedCookie.toString())
}
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
if (EhUrl.DOMAIN_E in cookie.domain) manager.setCookie(EhUrl.HOST_E, cookie.toString()) else manager.setCookie(EhUrl.DOMAIN_EX, cookie.toString())
}
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookies = get(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
@@ -159,56 +114,8 @@ object EhCookieStore : CookieJar {
return cookieHeader.toString()
}
@Synchronized
fun getCookies(url: HttpUrl): List<Cookie> {
val accepted: MutableList<Cookie> = ArrayList()
val expired: MutableList<Cookie> = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
db.remove(cookie)
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (cookie.name == name) {
return true
}
}
return false
}
/**
* Remove all cookies in this `CookieRepository`.
*/
@Synchronized
fun clear() {
map.clear()
launchIO {
db.clear()
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
if (cookie.name != "__utmp") {
addCookie(cookie)
}
}
}
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.filterNot { it.name == KEY_UTMP_NAME }.forEach { manager.setCookie(url.toString(), it.toString()) }
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
@@ -221,8 +128,7 @@ object EhCookieStore : CookieJar {
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
private val VERIFY_AS_IP_ADDRESS = Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
@@ -238,32 +144,19 @@ object EhCookieStore : CookieJar {
return if (urlHost == domain) {
true // As in 'example.com' matching 'example.com'.
} else {
urlHost.endsWith(domain!!) &&
urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
urlHost,
)
urlHost.endsWith(domain!!) && urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(urlHost)
}
// As in 'example.com' matching 'www.example.com'.
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = getCookies(url)
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
return if (checkTips) {
val result: MutableList<Cookie> = ArrayList(cookies.size + 1)
// Add all but skip some
for (cookie in cookies) {
val name = cookie.name
if (KEY_CONTENT_WARNING == name) {
continue
}
result.add(cookie)
return get(url).run {
if (checkTips) {
filterNot { it.name == KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
} else {
this
}
// Add some
result.add(sTipsCookie)
Collections.unmodifiableList(result)
} else {
cookies
}
}
}
@@ -0,0 +1,37 @@
package com.hippo.ehviewer.legacy
import arrow.fx.coroutines.release
import arrow.fx.coroutines.resource
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.dao.Cookie
import com.hippo.ehviewer.dao.CookiesDatabase
import splitties.arch.room.roomDb
import splitties.init.appCtx
private const val legacyCookiesDB = "okhttp3-cookie.db"
private fun Cookie.toOkHttp3Cookie(): okhttp3.Cookie {
return okhttp3.Cookie.Builder().apply {
name(name)
value(value)
expiresAt(expiresAt)
if (hostOnly) hostOnlyDomain(domain) else domain(domain)
path(path)
if (secure) secure()
if (httpOnly) httpOnly()
}.build()
}
suspend fun migrateCookies() {
if (legacyCookiesDB !in appCtx.databaseList()) return
resource {
roomDb<CookiesDatabase>(legacyCookiesDB)
} release {
it.close()
appCtx.deleteDatabase(legacyCookiesDB)
} use { database ->
database.cookiesDao().list().forEach {
EhCookieStore.addCookie(it.toOkHttp3Cookie())
}
}
}
@@ -1,91 +0,0 @@
/*
* Copyright 2017 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.network
import android.content.Context
import com.hippo.ehviewer.dao.Cookie
import com.hippo.ehviewer.dao.CookiesDatabase
import splitties.arch.room.roomDb
import okhttp3.Cookie as OkHttpCookie
class CookieDatabase(context: Context) {
private val cookiesList by lazy {
val now = System.currentTimeMillis()
db.cookiesDao().list().mapNotNull {
it.takeUnless { !it.persistent || it.expiresAt <= now } ?: run {
db.cookiesDao().delete(it)
null
}
}.toMutableList()
}
private val db = roomDb<CookiesDatabase>("okhttp3-cookie.db")
val allCookies by lazy {
hashMapOf<String, CookieSet>().also { map ->
cookiesList.map { it.toOkHttp3Cookie() }.forEach {
val set = map[it.domain] ?: CookieSet().apply { map[it.domain] = this }
set.add(it)
}
}
}
private fun findCookieWithOkHttpCookies(cookie: OkHttpCookie): Cookie? {
return cookiesList.find { it.name == cookie.name && it.domain == cookie.domain && it.value == cookie.value }
}
fun add(cookie: OkHttpCookie) {
val c = cookie.toCookie()
c.id = db.cookiesDao().insert(c)
cookiesList.add(c)
}
fun update(from: OkHttpCookie, to: OkHttpCookie) {
val origin = findCookieWithOkHttpCookies(from) ?: return
val new = to.toCookie(origin.id)
cookiesList.remove(origin)
cookiesList.add(new)
db.cookiesDao().update(new)
}
fun remove(cookie: OkHttpCookie) {
val origin = findCookieWithOkHttpCookies(cookie) ?: return
db.cookiesDao().delete(origin)
cookiesList.remove(origin)
}
fun clear() {
db.clearAllTables()
cookiesList.clear()
}
}
fun Cookie.toOkHttp3Cookie(): OkHttpCookie {
return OkHttpCookie.Builder().apply {
name(name)
value(value)
expiresAt(expiresAt)
if (hostOnly) hostOnlyDomain(domain) else domain(domain)
path(path)
if (secure) secure()
if (httpOnly) httpOnly()
}.build()
}
private fun OkHttpCookie.toCookie(id: Long? = null): Cookie {
return Cookie(
name, value, expiresAt, domain, path, secure, httpOnly, persistent, hostOnly, id,
)
}
@@ -1,68 +0,0 @@
/*
* Copyright 2017 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.network
import okhttp3.Cookie
import okhttp3.HttpUrl
class CookieSet {
private val map: MutableMap<Key, Cookie> = HashMap()
/**
* Adds a cookie to this `CookieSet`.
* Returns a previous cookie with
* the same name, domain and path or `null`.
*/
fun add(cookie: Cookie): Cookie? {
return map.put(Key(cookie), cookie)
}
/**
* Removes a cookie with the same name,
* domain and path as the cookie.
* Returns the removed cookie or `null`.
*/
fun remove(cookie: Cookie): Cookie? {
return map.remove(Key(cookie))
}
/**
* Get cookies for the url. Fill `accepted` and `expired`.
*/
operator fun get(url: HttpUrl, accepted: MutableList<Cookie>, expired: MutableList<Cookie>) {
val now = System.currentTimeMillis()
val iterator = map.entries.iterator()
while (iterator.hasNext()) {
val cookie = iterator.next().value
if (cookie.expiresAt <= now) {
iterator.remove()
expired.add(cookie)
} else if (cookie.matches(url)) {
accepted.add(cookie)
}
}
}
fun get(name: String, domain: String, path: String) = map[Key(name, domain, path)]
internal data class Key(
val name: String,
val domain: String,
val path: String,
) {
constructor(cookie: Cookie) : this(cookie.name, cookie.domain, cookie.path)
}
}
@@ -21,7 +21,7 @@ suspend fun postLogin() = coroutineScope {
runCatching {
// For the `star` cookie
EhEngine.getNews(false)
EhCookieStore.copyCookie(EhUrl.DOMAIN_E, EhUrl.DOMAIN_EX, EhCookieStore.KEY_STAR)
EhCookieStore.copyNecessaryCookies()
// Sad panda check
Settings.gallerySite = EhUrl.SITE_EX
@@ -21,8 +21,6 @@ import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
@SuppressLint("SetJavaScriptEnabled")
@@ -34,63 +32,24 @@ fun WebviewSignInScreen() {
val client = remember {
object : AccompanistWebViewClient() {
private var present = false
fun parseCookies(url: HttpUrl?, cookieStrings: String?): List<Cookie> {
if (cookieStrings == null) {
return emptyList()
}
var cookies: MutableList<Cookie>? = null
val pieces =
cookieStrings.split(";".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
for (piece in pieces) {
val cookie = Cookie.parse(url!!, piece) ?: continue
if (cookies == null) {
cookies = ArrayList()
}
cookies.add(cookie)
}
return cookies ?: emptyList()
}
fun addCookie(domain: String, cookie: Cookie) {
EhCookieStore.addCookie(
EhCookieStore.newCookie(
cookie,
domain,
forcePersistent = true,
forceLongLive = true,
forceNotHostOnly = true,
),
)
}
override fun onPageFinished(view: WebView, url: String?) {
if (present) {
view.destroy()
return
}
val httpUrl = url?.toHttpUrl() ?: return
val cookieString = CookieManager.getInstance().getCookie(EhUrl.HOST_E)
val cookies = parseCookies(httpUrl, cookieString)
var getId = false
var getHash = false
for (cookie in cookies) {
if (EhCookieStore.KEY_IPB_MEMBER_ID == cookie.name) {
EhCookieStore.loadForRequest(EhUrl.HOST_E.toHttpUrl()).forEach {
if (EhCookieStore.KEY_IPB_MEMBER_ID == it.name) {
getId = true
} else if (EhCookieStore.KEY_IPB_PASS_HASH == cookie.name) {
} else if (EhCookieStore.KEY_IPB_PASS_HASH == it.name) {
getHash = true
}
}
if (getId && getHash) {
present = true
coroutineScope.launchIO {
val canEx = withNonCancellableContext {
cookies.forEach {
addCookie(EhUrl.DOMAIN_EX, it)
addCookie(EhUrl.DOMAIN_E, it)
}
postLogin()
}
val canEx = withNonCancellableContext { postLogin() }
withUIContext { navController.navigate(if (canEx) SELECT_SITE_ROUTE_NAME else FINISH_ROUTE_NAME) }
}
}
@@ -91,8 +91,8 @@ fun EhScreen() {
title = stringResource(id = R.string.account_name),
summary = Settings.displayName ?: touristMode,
) {
val eCookies = EhCookieStore.getCookies(EhUrl.HOST_E.toHttpUrl())
val exCookies = EhCookieStore.getCookies(EhUrl.HOST_EX.toHttpUrl())
val eCookies = EhCookieStore.loadForRequest(EhUrl.HOST_E.toHttpUrl())
val exCookies = EhCookieStore.loadForRequest(EhUrl.HOST_EX.toHttpUrl())
var ipbMemberId: String? = null
var ipbPassHash: String? = null
var igneous: String? = null
@@ -1,7 +1,6 @@
package com.hippo.ehviewer.ui.settings
import android.annotation.SuppressLint
import android.webkit.CookieManager
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@@ -12,16 +11,13 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.ui.LocalNavController
import okhttp3.HttpUrl.Companion.toHttpUrl
@SuppressLint("SetJavaScriptEnabled")
@Composable
@@ -52,16 +48,5 @@ fun MyTagsScreen() {
}
},
)
SideEffect {
CookieManager.getInstance().apply {
flush()
removeAllCookies(null)
removeSessionCookies(null)
// Copy cookies from okhttp cookie store to CookieManager
EhCookieStore.getCookies(url.toHttpUrl()).forEach {
setCookie(url, it.toString())
}
}
}
}
}
@@ -1,7 +1,6 @@
package com.hippo.ehviewer.ui.settings
import android.annotation.SuppressLint
import android.webkit.CookieManager
import android.webkit.WebView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -29,8 +28,6 @@ import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhCookieStore.KEY_SETTINGS_PROFILE
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.ui.LocalNavController
import eu.kanade.tachiyomi.util.lang.launchIO
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
private const val applyJs = "javascript:(function(){var apply = document.getElementById(\"apply\").children[0];apply.click();})();"
@@ -81,33 +78,9 @@ fun UConfigScreen() {
val applyTip = stringResource(id = R.string.apply_tip)
LaunchedEffect(Unit) { snackbarHostState.showSnackbar(applyTip) }
DisposableEffect(Unit) {
CookieManager.getInstance().apply {
flush()
removeAllCookies(null)
removeSessionCookies(null)
// Copy cookies from okhttp cookie store to CookieManager
EhCookieStore.getCookies(url.toHttpUrl()).forEach {
setCookie(url, it.toString())
}
}
onDispose {
// Put cookies back to okhttp cookie store
val cookiesString = CookieManager.getInstance().getCookie(url)
if (cookiesString.isNotBlank()) {
val hostUrl = EhUrl.host.toHttpUrl()
launchIO {
EhCookieStore.deleteCookie(hostUrl, KEY_SETTINGS_PROFILE)
// The cookies saved in the uconfig page should not be shared between e and ex
for (header in cookiesString.split(";".toRegex()).dropLastWhile { it.isEmpty() }) {
Cookie.parse(hostUrl, header)?.let {
EhCookieStore.addCookie(longLive(it))
}
}
}
}
EhCookieStore.deleteCookie(EhUrl.host.toHttpUrl(), KEY_SETTINGS_PROFILE)
}
}
}
}
private fun longLive(cookie: Cookie) = Cookie.Builder().name(cookie.name).value(cookie.value).domain(cookie.domain).path(cookie.path).expiresAt(Long.MAX_VALUE).build()