Use system CookieManager as CookieJar

This commit is contained in:
Koitharu
2021-01-20 07:50:35 +02:00
parent 8f8d85d172
commit 96d437b2a8
13 changed files with 146 additions and 566 deletions

View File

@@ -1,23 +1,19 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.graphics.Bitmap
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.WebViewClientCompat
class CloudFlareClient(
private val cookieJar: CookieJar,
private val cookieJar: AndroidCookieJar,
private val callback: CloudFlareCallback,
private val targetUrl: String
) : WebViewClient() {
private val cookieManager = CookieManager.getInstance()
) : WebViewClientCompat() {
init {
cookieManager.removeAllCookies(null)
cookieJar.remove(targetUrl, CF_UID, CF_CLEARANCE)
}
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
@@ -36,16 +32,11 @@ class CloudFlareClient(
}
private fun checkClearance() {
val httpUrl = targetUrl.toHttpUrl()
val rawCookie = cookieManager.getCookie(targetUrl) ?: return
val cookies = rawCookie.split(';').mapNotNull {
Cookie.parse(httpUrl, it)
val cookies = cookieJar.loadForRequest(targetUrl.toHttpUrl())
if (cookies.any { it.name == CF_CLEARANCE }) {
callback.onCheckPassed()
}
if (cookies.none { it.name == CF_CLEARANCE }) {
return
}
cookieJar.saveFromResponse(httpUrl, cookies)
callback.onCheckPassed()
}
private companion object {

View File

@@ -0,0 +1,47 @@
package org.koitharu.kotatsu.core.network
import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class AndroidCookieJar : CookieJar {
private val cookieManager = CookieManager.getInstance()
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
return rawCookie.split(';').mapNotNull {
Cookie.parse(url, it)
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
if (cookies.isEmpty()) {
return
}
val urlString = url.toString()
for (cookie in cookies) {
cookieManager.setCookie(urlString, cookie.toString())
}
}
fun remove(url: String, vararg names: String) {
val cookies = cookieManager.getCookie(url) ?: return
val newCookies = cookies.split(";")
.filterNot { cookie ->
names.any { cookie.startsWith("$it=") }
}.joinToString(";")
cookieManager.setCookie(url, newCookies)
}
fun clearAsync() {
cookieManager.removeAllCookies(null)
}
suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
cookieManager.removeAllCookies(continuation::resume)
}
}

View File

@@ -6,21 +6,12 @@ import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.core.network.cookies.ClearableCookieJar
import org.koitharu.kotatsu.core.network.cookies.PersistentCookieJar
import org.koitharu.kotatsu.core.network.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.network.cookies.persistence.SharedPrefsCookiePersistor
import org.koitharu.kotatsu.utils.CacheUtils
import java.util.concurrent.TimeUnit
val networkModule
get() = module {
single<CookieJar> {
PersistentCookieJar(
SetCookieCache(),
SharedPrefsCookiePersistor(androidContext())
)
} bind ClearableCookieJar::class
single { AndroidCookieJar() } bind CookieJar::class
single(named(CacheUtils.QUALIFIER_HTTP)) { CacheUtils.createHttpCache(androidContext()) }
single {
OkHttpClient.Builder().apply {

View File

@@ -0,0 +1,86 @@
package org.koitharu.kotatsu.core.network
import android.annotation.TargetApi
import android.os.Build
import android.webkit.*
@Suppress("OverridingDeprecatedMember")
abstract class WebViewClientCompat : WebViewClient() {
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
return false
}
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
return null
}
open fun onReceivedErrorCompat(
view: WebView,
errorCode: Int,
description: String?,
failingUrl: String,
isMainFrame: Boolean
) {
}
@TargetApi(Build.VERSION_CODES.N)
final override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean = shouldOverrideUrlCompat(view, request.url.toString())
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
return shouldOverrideUrlCompat(view, url)
}
final override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? = shouldInterceptRequestCompat(view, request.url.toString())
final override fun shouldInterceptRequest(
view: WebView,
url: String
): WebResourceResponse? = shouldInterceptRequestCompat(view, url)
@TargetApi(Build.VERSION_CODES.M)
final override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
onReceivedErrorCompat(
view,
error.errorCode,
error.description?.toString(),
request.url.toString(),
request.isForMainFrame
)
}
final override fun onReceivedError(
view: WebView,
errorCode: Int,
description: String?,
failingUrl: String
) {
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
}
@TargetApi(Build.VERSION_CODES.M)
final override fun onReceivedHttpError(
view: WebView,
request: WebResourceRequest,
error: WebResourceResponse
) {
onReceivedErrorCompat(
view,
error.statusCode,
error.reasonPhrase,
request.url
.toString(),
request.isForMainFrame
)
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies
import okhttp3.CookieJar
/**
* This interface extends [okhttp3.CookieJar] and adds methods to clear the cookies.
*/
interface ClearableCookieJar : CookieJar {
/**
* Clear all the session cookies while maintaining the persisted ones.
*/
fun clearSession()
/**
* Clear all the cookies from persistence and from the cache.
*/
fun clear()
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies
import okhttp3.Cookie
import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.network.cookies.cache.CookieCache
import org.koitharu.kotatsu.core.network.cookies.persistence.CookiePersistor
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
class PersistentCookieJar(
private val cache: CookieCache,
private val persistor: CookiePersistor
) : ClearableCookieJar {
private var isLoaded = AtomicBoolean(false)
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
if (isLoaded.compareAndSet(false, true)) {
cache.addAll(persistor.loadAll())
}
cache.addAll(cookies)
persistor.saveAll(cookies.filter { it.persistent })
}
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
if (isLoaded.compareAndSet(false, true)) {
cache.addAll(persistor.loadAll())
}
val cookiesToRemove: MutableList<Cookie> = ArrayList()
val validCookies: MutableList<Cookie> = ArrayList()
val it = cache.iterator()
while (it.hasNext()) {
val currentCookie = it.next()
when {
currentCookie.isExpired() -> {
cookiesToRemove.add(currentCookie)
it.remove()
}
currentCookie.matches(url) -> {
validCookies.add(currentCookie)
}
}
}
persistor.removeAll(cookiesToRemove)
return validCookies
}
@Synchronized
override fun clearSession() {
cache.clear()
cache.addAll(persistor.loadAll())
}
@Synchronized
override fun clear() {
cache.clear()
persistor.clear()
}
private companion object {
fun Cookie.isExpired(): Boolean {
return expiresAt < System.currentTimeMillis()
}
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies.cache
import okhttp3.Cookie
/**
* A CookieCache handles the volatile cookie session storage.
*/
interface CookieCache : MutableIterable<Cookie> {
/**
* Add all the new cookies to the session, existing cookies will be overwritten.
*
* @param newCookies
*/
fun addAll(newCookies: Collection<Cookie>)
/**
* Clear all the cookies from the session.
*/
fun clear()
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies.cache
import okhttp3.Cookie
import java.util.*
/**
* This class decorates a Cookie to re-implements equals() and hashcode() methods in order to identify
* the cookie by the following attributes: name, domain, path, secure & hostOnly.
*
*
*
* This new behaviour will be useful in determining when an already existing cookie in session must be overwritten.
*/
internal class IdentifiableCookie(val cookie: Cookie) {
override fun equals(other: Any?): Boolean {
if (other !is IdentifiableCookie) return false
return other.cookie.name == cookie.name && other.cookie.domain == cookie.domain
&& other.cookie.path == cookie.path && other.cookie.secure == cookie.secure
&& other.cookie.hostOnly == cookie.hostOnly
}
override fun hashCode(): Int {
var hash = 17
hash = 31 * hash + cookie.name.hashCode()
hash = 31 * hash + cookie.domain.hashCode()
hash = 31 * hash + cookie.path.hashCode()
hash = 31 * hash + if (cookie.secure) 0 else 1
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
return hash
}
companion object {
fun decorateAll(cookies: Collection<Cookie>): List<IdentifiableCookie> {
val identifiableCookies: MutableList<IdentifiableCookie> = ArrayList(cookies.size)
for (cookie in cookies) {
identifiableCookies.add(IdentifiableCookie(cookie))
}
return identifiableCookies
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies.cache
import okhttp3.Cookie
import org.koitharu.kotatsu.core.network.cookies.cache.IdentifiableCookie.Companion.decorateAll
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class SetCookieCache : CookieCache {
private val cookies: MutableSet<IdentifiableCookie> =
Collections.newSetFromMap(ConcurrentHashMap())
override fun addAll(newCookies: Collection<Cookie>) {
for (cookie in decorateAll(newCookies)) {
cookies.remove(cookie)
cookies.add(cookie)
}
}
override fun clear() {
cookies.clear()
}
override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator()
private inner class SetCookieCacheIterator : MutableIterator<Cookie> {
private val iterator = cookies.iterator()
override fun hasNext(): Boolean {
return iterator.hasNext()
}
override fun next(): Cookie {
return iterator.next().cookie
}
override fun remove() {
iterator.remove()
}
}
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies.persistence
import okhttp3.Cookie
/**
* A CookiePersistor handles the persistent cookie storage.
*/
interface CookiePersistor {
fun loadAll(): List<Cookie>
/**
* Persist all cookies, existing cookies will be overwritten.
*
* @param cookies cookies persist
*/
fun saveAll(cookies: Collection<Cookie>)
/**
* Removes indicated cookies from persistence.
*
* @param cookies cookies to remove from persistence
*/
fun removeAll(cookies: Collection<Cookie>)
/**
* Clear all cookies from persistence.
*/
fun clear()
}

View File

@@ -1,150 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies.persistence
import android.util.Log
import okhttp3.Cookie
import java.io.*
class SerializableCookie : Serializable {
@Transient
private var cookie: Cookie? = null
fun encode(cookie: Cookie?): String? {
this.cookie = cookie
val byteArrayOutputStream = ByteArrayOutputStream()
var objectOutputStream: ObjectOutputStream? = null
try {
objectOutputStream = ObjectOutputStream(byteArrayOutputStream)
objectOutputStream.writeObject(this)
} catch (e: IOException) {
Log.d(TAG, "IOException in encodeCookie", e)
return null
} finally {
if (objectOutputStream != null) {
try { // Closing a ByteArrayOutputStream has no effect, it can be used later (and is used in the return statement)
objectOutputStream.close()
} catch (e: IOException) {
Log.d(TAG, "Stream not closed in encodeCookie", e)
}
}
}
return byteArrayToHexString(byteArrayOutputStream.toByteArray())
}
fun decode(encodedCookie: String): Cookie? {
val bytes = hexStringToByteArray(encodedCookie)
val byteArrayInputStream = ByteArrayInputStream(
bytes
)
var cookie: Cookie? = null
var objectInputStream: ObjectInputStream? = null
try {
objectInputStream = ObjectInputStream(byteArrayInputStream)
cookie = (objectInputStream.readObject() as SerializableCookie).cookie
} catch (e: IOException) {
Log.d(TAG, "IOException in decodeCookie", e)
} catch (e: ClassNotFoundException) {
Log.d(TAG, "ClassNotFoundException in decodeCookie", e)
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close()
} catch (e: IOException) {
Log.d(TAG, "Stream not closed in decodeCookie", e)
}
}
}
return cookie
}
@Throws(IOException::class)
private fun writeObject(out: ObjectOutputStream) {
out.writeObject(cookie!!.name)
out.writeObject(cookie!!.value)
out.writeLong(if (cookie!!.persistent) cookie!!.expiresAt else NON_VALID_EXPIRES_AT)
out.writeObject(cookie!!.domain)
out.writeObject(cookie!!.path)
out.writeBoolean(cookie!!.secure)
out.writeBoolean(cookie!!.httpOnly)
out.writeBoolean(cookie!!.hostOnly)
}
@Throws(IOException::class, ClassNotFoundException::class)
private fun readObject(`in`: ObjectInputStream) {
val builder = Cookie.Builder()
builder.name((`in`.readObject() as String))
builder.value((`in`.readObject() as String))
val expiresAt = `in`.readLong()
if (expiresAt != NON_VALID_EXPIRES_AT) {
builder.expiresAt(expiresAt)
}
val domain = `in`.readObject() as String
builder.domain(domain)
builder.path((`in`.readObject() as String))
if (`in`.readBoolean()) builder.secure()
if (`in`.readBoolean()) builder.httpOnly()
if (`in`.readBoolean()) builder.hostOnlyDomain(domain)
cookie = builder.build()
}
private companion object {
private val TAG = SerializableCookie::class.java.simpleName
const val serialVersionUID = -8594045714036645534L
private const val NON_VALID_EXPIRES_AT = -1L
/**
* Using some super basic byte array &lt;-&gt; hex conversions so we don't
* have to rely on any large Base64 libraries. Can be overridden if you
* like!
*
* @param bytes byte array to be converted
* @return string containing hex values
*/
private fun byteArrayToHexString(bytes: ByteArray): String {
val sb = StringBuilder(bytes.size * 2)
for (element in bytes) {
val v: Int = element.toInt() and 0xff
if (v < 16) {
sb.append('0')
}
sb.append(Integer.toHexString(v))
}
return sb.toString()
}
/**
* Converts hex values from strings to byte array
*
* @param hexString string of hex-encoded values
* @return decoded byte array
*/
private fun hexStringToByteArray(hexString: String): ByteArray {
val len = hexString.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + Character
.digit(hexString[i + 1], 16)).toByte()
i += 2
}
return data
}
}
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright (C) 2016 Francisco José Montiel Navarro.
*
* 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 org.koitharu.kotatsu.core.network.cookies.persistence
import android.content.Context
import okhttp3.Cookie
import java.util.*
class SharedPrefsCookiePersistor(context: Context) : CookiePersistor {
private val sharedPreferences by lazy {
context.getSharedPreferences("cookies", Context.MODE_PRIVATE)
}
override fun loadAll(): List<Cookie> {
val cookies: MutableList<Cookie> = ArrayList(sharedPreferences.all.size)
for ((_, value) in sharedPreferences.all) {
val serializedCookie = value as? String
if (serializedCookie != null) {
val cookie = SerializableCookie().decode(serializedCookie)
if (cookie != null) {
cookies.add(cookie)
}
}
}
return cookies
}
override fun saveAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit()
for (cookie in cookies) {
editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie))
}
editor.apply()
}
override fun removeAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit()
for (cookie in cookies) {
editor.remove(createCookieKey(cookie))
}
editor.apply()
}
override fun clear() {
sharedPreferences.edit().clear().apply()
}
private companion object {
fun createCookieKey(cookie: Cookie): String {
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
}
}
}

View File

@@ -11,7 +11,7 @@ import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.network.cookies.ClearableCookieJar
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.Cache
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
@@ -75,10 +75,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
AppSettings.KEY_COOKIES_CLEAR -> {
viewLifecycleScope.launch {
val cookieJar = get<ClearableCookieJar>()
withContext(Dispatchers.IO) {
cookieJar.clear()
}
val cookieJar = get<AndroidCookieJar>()
cookieJar.clear()
Snackbar.make(
listView ?: return@launch,
R.string.cookies_cleared,