From f02c23329201e5e364cd2222421976ec8aad0848 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 Feb 2020 21:55:14 +0200 Subject: [PATCH] Cookie jar --- app/src/main/AndroidManifest.xml | 1 + .../java/org/koitharu/kotatsu/KotatsuApp.kt | 8 + .../persistentcookiejar/ClearableCookieJar.kt | 34 ++++ .../PersistentCookieJar.kt | 87 ++++++++++ .../persistentcookiejar/cache/CookieCache.kt | 35 ++++ .../cache/IdentifiableCookie.kt | 60 +++++++ .../cache/SetCookieCache.kt | 57 +++++++ .../persistence/CookiePersistor.kt | 44 +++++ .../persistence/SerializableCookie.kt | 150 ++++++++++++++++++ .../persistence/SharedPrefsCookiePersistor.kt | 74 +++++++++ 10 files changed, 550 insertions(+) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/ClearableCookieJar.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/PersistentCookieJar.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/CookieCache.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/IdentifiableCookie.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/SetCookieCache.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/CookiePersistor.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SerializableCookie.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SharedPrefsCookiePersistor.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 72e881137..50907769f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 835585831..22deb6a4f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -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, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/ClearableCookieJar.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/ClearableCookieJar.kt new file mode 100644 index 000000000..da912d32e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/ClearableCookieJar.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/PersistentCookieJar.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/PersistentCookieJar.kt new file mode 100644 index 000000000..b99cae53a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/PersistentCookieJar.kt @@ -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) { + cache.addAll(cookies) + persistor.saveAll(filterPersistentCookies(cookies)) + } + + @Synchronized + override fun loadForRequest(url: HttpUrl): List { + val cookiesToRemove: MutableList = ArrayList() + val validCookies: MutableList = 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): List { + val persistentCookies: MutableList = ArrayList() + for (cookie in cookies) { + if (cookie.persistent) { + persistentCookies.add(cookie) + } + } + return persistentCookies + } + + @JvmStatic + fun isCookieExpired(cookie: Cookie): Boolean { + return cookie.expiresAt < System.currentTimeMillis() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/CookieCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/CookieCache.kt new file mode 100644 index 000000000..523c234da --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/CookieCache.kt @@ -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 { + /** + * Add all the new cookies to the session, existing cookies will be overwritten. + * + * @param newCookies + */ + fun addAll(newCookies: Collection) + + /** + * Clear all the cookies from the session. + */ + fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/IdentifiableCookie.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/IdentifiableCookie.kt new file mode 100644 index 000000000..0579db1ef --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/IdentifiableCookie.kt @@ -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): List { + val identifiableCookies: MutableList = ArrayList(cookies.size) + for (cookie in cookies) { + identifiableCookies.add(IdentifiableCookie(cookie)) + } + return identifiableCookies + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/SetCookieCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/SetCookieCache.kt new file mode 100644 index 000000000..9ad8b8d2a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/cache/SetCookieCache.kt @@ -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 = Collections.newSetFromMap(ConcurrentHashMap()) + + override fun addAll(newCookies: Collection) { + for (cookie in decorateAll(newCookies)) { + cookies.remove(cookie) + cookies.add(cookie) + } + } + + override fun clear() { + cookies.clear() + } + + override fun iterator(): MutableIterator = SetCookieCacheIterator() + + private inner class SetCookieCacheIterator internal constructor() : MutableIterator { + + 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() + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/CookiePersistor.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/CookiePersistor.kt new file mode 100644 index 000000000..8c33bd79b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/CookiePersistor.kt @@ -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 + /** + * Persist all cookies, existing cookies will be overwritten. + * + * @param cookies cookies persist + */ + fun saveAll(cookies: Collection) + + /** + * Removes indicated cookies from persistence. + * + * @param cookies cookies to remove from persistence + */ + fun removeAll(cookies: Collection) + + /** + * Clear all cookies from persistence. + */ + fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SerializableCookie.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SerializableCookie.kt new file mode 100644 index 000000000..3ba44dfca --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SerializableCookie.kt @@ -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 <-> 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SharedPrefsCookiePersistor.kt b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SharedPrefsCookiePersistor.kt new file mode 100644 index 000000000..42aa74feb --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/http/persistentcookiejar/persistence/SharedPrefsCookiePersistor.kt @@ -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 { + val cookies: MutableList = 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) { + val editor = sharedPreferences.edit() + for (cookie in cookies) { + editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie)) + } + editor.commit() + } + + @SuppressLint("ApplySharedPref") + override fun removeAll(cookies: Collection) { + 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 + } + } + +} \ No newline at end of file