Cookie jar

This commit is contained in:
Koitharu
2020-02-23 21:55:14 +02:00
parent 0e746091b8
commit f02c233292
10 changed files with 550 additions and 0 deletions

View File

@@ -14,6 +14,7 @@
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

View File

@@ -12,6 +12,9 @@ import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.http.persistentcookiejar.PersistentCookieJar
import org.koitharu.kotatsu.core.http.persistentcookiejar.cache.SetCookieCache
import org.koitharu.kotatsu.core.http.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -20,6 +23,10 @@ import java.util.concurrent.TimeUnit
class KotatsuApp : Application() {
private val cookieJar by lazy {
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
}
override fun onCreate() {
super.onCreate()
initKoin()
@@ -74,6 +81,7 @@ class KotatsuApp : Application() {
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.cookieJar(cookieJar)
private fun mangaDb() = Room.databaseBuilder(
applicationContext,

View File

@@ -0,0 +1,34 @@
/*
* 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.http.persistentcookiejar
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

@@ -0,0 +1,87 @@
/*
* 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.http.persistentcookiejar
import org.koitharu.kotatsu.core.http.persistentcookiejar.persistence.CookiePersistor
import okhttp3.Cookie
import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.http.persistentcookiejar.cache.CookieCache
import java.util.*
class PersistentCookieJar(
private val cache: CookieCache,
private val persistor: CookiePersistor
) : ClearableCookieJar {
init {
cache.addAll(persistor.loadAll())
}
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cache.addAll(cookies)
persistor.saveAll(filterPersistentCookies(cookies))
}
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookiesToRemove: MutableList<Cookie> = ArrayList()
val validCookies: MutableList<Cookie> = ArrayList()
val it = cache.iterator()
while (it.hasNext()) {
val currentCookie = it.next()
if (isCookieExpired(currentCookie)) {
cookiesToRemove.add(currentCookie)
it.remove()
} else if (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 {
@JvmStatic
fun filterPersistentCookies(cookies: List<Cookie>): List<Cookie> {
val persistentCookies: MutableList<Cookie> = ArrayList()
for (cookie in cookies) {
if (cookie.persistent) {
persistentCookies.add(cookie)
}
}
return persistentCookies
}
@JvmStatic
fun isCookieExpired(cookie: Cookie): Boolean {
return cookie.expiresAt < System.currentTimeMillis()
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.http.persistentcookiejar.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

@@ -0,0 +1,60 @@
/*
* 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.http.persistentcookiejar.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 {
@JvmStatic
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

@@ -0,0 +1,57 @@
/*
* 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.http.persistentcookiejar.cache
import okhttp3.Cookie
import org.koitharu.kotatsu.core.http.persistentcookiejar.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 internal constructor() : 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

@@ -0,0 +1,44 @@
/*
* 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.http.persistentcookiejar.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

@@ -0,0 +1,150 @@
/*
* 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.http.persistentcookiejar.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
*/
@JvmStatic
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
*/
@JvmStatic
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

@@ -0,0 +1,74 @@
/*
* 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.http.persistentcookiejar.persistence
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import okhttp3.Cookie
import java.util.*
@SuppressLint("CommitPrefEdits")
class SharedPrefsCookiePersistor(private val sharedPreferences: SharedPreferences) :
CookiePersistor {
constructor(context: Context) : this(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
}
@SuppressLint("ApplySharedPref")
override fun saveAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit()
for (cookie in cookies) {
editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie))
}
editor.commit()
}
@SuppressLint("ApplySharedPref")
override fun removeAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit()
for (cookie in cookies) {
editor.remove(createCookieKey(cookie))
}
editor.commit()
}
@SuppressLint("ApplySharedPref")
override fun clear() {
sharedPreferences.edit().clear().commit()
}
private companion object {
fun createCookieKey(cookie: Cookie): String {
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
}
}
}