diff --git a/app/schemas/com.hippo.ehviewer.dao.CookiesDatabase/1.json b/app/schemas/com.hippo.ehviewer.dao.CookiesDatabase/1.json deleted file mode 100644 index a0909aa08..000000000 --- a/app/schemas/com.hippo.ehviewer.dao.CookiesDatabase/1.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "c801ee62729ba20a203b150211234d5c", - "entities": [ - { - "tableName": "OK_HTTP_3_COOKIE", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT, `VALUE` TEXT, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT, `PATH` TEXT, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "NAME", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "value", - "columnName": "VALUE", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "expiresAt", - "columnName": "EXPIRES_AT", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "domain", - "columnName": "DOMAIN", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "path", - "columnName": "PATH", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "secure", - "columnName": "SECURE", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "httpOnly", - "columnName": "HTTP_ONLY", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "persistent", - "columnName": "PERSISTENT", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "hostOnly", - "columnName": "HOST_ONLY", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "_id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c801ee62729ba20a203b150211234d5c')" - ] - } -} \ No newline at end of file diff --git a/app/schemas/com.hippo.ehviewer.dao.CookiesDatabase/2.json b/app/schemas/com.hippo.ehviewer.dao.CookiesDatabase/2.json deleted file mode 100644 index 7726ebe23..000000000 --- a/app/schemas/com.hippo.ehviewer.dao.CookiesDatabase/2.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "1b80cd29939b0f43934721f1289ba94d", - "entities": [ - { - "tableName": "OK_HTTP_3_COOKIE", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT NOT NULL, `VALUE` TEXT NOT NULL, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT NOT NULL, `PATH` TEXT NOT NULL, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "NAME", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "VALUE", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "expiresAt", - "columnName": "EXPIRES_AT", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "domain", - "columnName": "DOMAIN", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "PATH", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "secure", - "columnName": "SECURE", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "httpOnly", - "columnName": "HTTP_ONLY", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "persistent", - "columnName": "PERSISTENT", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "hostOnly", - "columnName": "HOST_ONLY", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "_id", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "_id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b80cd29939b0f43934721f1289ba94d')" - ] - } -} \ No newline at end of file diff --git a/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt b/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt index 8cfe26072..813361f1e 100644 --- a/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt +++ b/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt @@ -13,7 +13,6 @@ import kotlin.coroutines.resumeWithException import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.RequestBody import okio.Buffer import okio.Path.Companion.toOkioPath @@ -64,7 +63,7 @@ class CronetRequest { inline fun cronetRequest(url: String, referer: String? = null, origin: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply { request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply { - addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl())) + addHeader("Cookie", EhCookieStore.getCookieHeader(url)) addHeader("Accept", CHROME_ACCEPT) addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE) referer?.let { addHeader("Referer", it) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index bfe30d6cc..970206670 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -34,12 +34,10 @@ import com.hippo.ehviewer.coil.installCronetHttpUriFetcher import com.hippo.ehviewer.dailycheck.checkDawn import com.hippo.ehviewer.dao.SearchDatabase import com.hippo.ehviewer.download.DownloadManager -import com.hippo.ehviewer.ktbuilder.cache import com.hippo.ehviewer.ktbuilder.diskCache import com.hippo.ehviewer.ktbuilder.httpClient import com.hippo.ehviewer.ktbuilder.imageLoader import com.hippo.ehviewer.legacy.cleanObsoleteCache -import com.hippo.ehviewer.legacy.migrateCookies import com.hippo.ehviewer.ui.keepNoMediaFileStatus import com.hippo.ehviewer.ui.lockObserver import com.hippo.ehviewer.util.AppConfig @@ -58,6 +56,8 @@ import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.cookies.HttpCookies +import io.ktor.http.HttpHeaders import kotlinx.coroutines.launch import moe.tarsin.kt.unreachable import okhttp3.AsyncDns @@ -98,9 +98,6 @@ class EhApplication : Application(), ImageLoaderFactory { System.loadLibrary("ehviewer_rust") ReadableTime.initialize(this) lifecycleScope.launchIO { - launchIO { - migrateCookies() - } launchIO { EhTagDatabase } @@ -164,7 +161,7 @@ class EhApplication : Application(), ImageLoaderFactory { callFactory { unreachable() } installCronetHttpUriFetcher() } else { - callFactory(baseOkHttpClient) + callFactory(coilClient) } if (isAtLeastP) { add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) } @@ -184,12 +181,21 @@ class EhApplication : Application(), ImageLoaderFactory { engine { preconfigured = baseOkHttpClient } + install(HttpCookies) { + storage = EhCookieStore + } } } - val baseOkHttpClient by lazy { + val noRedirectKtorClient by lazy { + HttpClient(ktorClient.engine) { + followRedirects = false + } + } + + // Fallback to CIO when cronet unavailable after coil 3.0 release + private val baseOkHttpClient by lazy { httpClient { - cookieJar(EhCookieStore) if (isAtLeastQ) { dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6)) } @@ -198,19 +204,16 @@ class EhApplication : Application(), ImageLoaderFactory { } } - val noRedirectOkHttpClient by lazy { + // Use KtorClient directly when coil 3.0 released + private val coilClient by lazy { httpClient(baseOkHttpClient) { - followRedirects(false) - } - } - - // Never use this okhttp client to download large blobs!!! - val okHttpClient by lazy { - httpClient(baseOkHttpClient) { - cache( - appCtx.cacheDir.toOkioPath() / "http_cache", - 20L * 1024L * 1024L, - ) + addInterceptor { + val req = it.request() + val newReq = req.newBuilder().apply { + addHeader(HttpHeaders.Cookie, EhCookieStore.getCookieHeader(req.url.toString())) + }.build() + it.proceed(newReq) + } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt index d44dbe9b5..6fdd23cab 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt @@ -1,50 +1,23 @@ -/* - * Copyright 2016 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.client import android.webkit.CookieManager import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastJoinToString -import androidx.compose.ui.util.fastMapNotNull -import okhttp3.Cookie -import okhttp3.CookieJar +import io.ktor.client.plugins.cookies.CookiesStorage +import io.ktor.http.Cookie +import io.ktor.http.Url +import io.ktor.http.parseClientCookiesHeader +import io.ktor.http.renderSetCookieHeader import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -object EhCookieStore : CookieJar { +object EhCookieStore : CookiesStorage { private val manager = CookieManager.getInstance() fun signOut() = manager.removeAllCookies(null) - fun contains(url: HttpUrl, name: String) = get(url).fastAny { it.name == name } - - fun get(url: HttpUrl): List { - val cookies = manager.getCookie(url.toString()) - - return if (!cookies.isNullOrEmpty()) { - cookies.split(";").fastMapNotNull { setCookie -> - Cookie.parse(url, setCookie)?.takeUnless { it.name == KEY_UTMP_NAME } - } - } else { - emptyList() - } - } + fun contains(url: String, name: String) = load(Url(url)).fastAny { it.name == name } fun hasSignedIn(): Boolean { - val url = EhUrl.HOST_E.toHttpUrl() + val url = EhUrl.HOST_E return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH) } @@ -55,16 +28,13 @@ object EhCookieStore : CookieJar { private const val KEY_CONTENT_WARNING = "nw" private const val CONTENT_WARNING_NOT_SHOW = "1" private const val KEY_UTMP_NAME = "__utmp" - private val sTipsCookie = Cookie.Builder().apply { - name(KEY_CONTENT_WARNING) - value(CONTENT_WARNING_NOT_SHOW) - domain(EhUrl.DOMAIN_E) - path("/") - expiresAt(Long.MAX_VALUE) - }.build() + private val sTipsCookie = Cookie( + name = KEY_CONTENT_WARNING, + value = CONTENT_WARNING_NOT_SHOW, + ) fun copyNecessaryCookies() { - val cookies = get(EhUrl.HOST_E.toHttpUrl()) + val cookies = load(Url(EhUrl.HOST_E)) cookies.fastForEach { if (it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH) { manager.setCookie(EhUrl.HOST_EX, it.toString()) @@ -76,33 +46,41 @@ object EhCookieStore : CookieJar { manager.setCookie(url.toString(), "$name=;Max-Age=0") } - fun addCookie(cookie: Cookie) { - manager.setCookie(if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX, cookie.toString()) + fun addCookie(k: String, v: String, domain: String) { + val cookie = Cookie(name = k, value = v, domain = domain, maxAge = Int.MAX_VALUE) + val url = if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX + manager.setCookie(url, renderSetCookieHeader(cookie)) } fun flush() = manager.flush() - fun getCookieHeader(url: HttpUrl): String { - return loadForRequest(url).fastJoinToString("; ") { + fun getCookieHeader(url: String): String { + return load(Url(url)).fastJoinToString("; ") { "${it.name}=${it.value}" } } // See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873 - override fun saveFromResponse(url: HttpUrl, cookies: List) = cookies.fastForEach { - if (it.name != KEY_UTMP_NAME) { - manager.setCookie(url.toString(), it.toString()) + override suspend fun addCookie(requestUrl: Url, cookie: Cookie) { + if (cookie.name != KEY_UTMP_NAME) { + manager.setCookie(requestUrl.toString(), renderSetCookieHeader(cookie)) } } - override fun loadForRequest(url: HttpUrl): List { + override fun close() = Unit + + fun load(url: Url): List { + val cookies = manager.getCookie(url.toString()) ?: return emptyList() val checkTips = EhUrl.DOMAIN_E in url.host - return get(url).run { - if (checkTips) { - fastFilter { it.name != KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) } - } else { - this + return parseClientCookiesHeader(cookies) + .map { Cookie(it.key, it.value, maxAge = Int.MAX_VALUE) } + .filterTo(mutableListOf()) { it.name != KEY_UTMP_NAME }.apply { + if (checkTips) { + removeAll { it.name == KEY_CONTENT_WARNING } + add(sTipsCookie) + } } - } } + + override suspend fun get(requestUrl: Url) = load(requestUrl) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt index 59d57b8e4..409ce5853 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt @@ -15,7 +15,6 @@ */ package com.hippo.ehviewer.client -import android.util.Log import arrow.core.Either import arrow.core.left import arrow.core.right @@ -48,20 +47,18 @@ import com.hippo.ehviewer.client.parser.TorrentParser import com.hippo.ehviewer.client.parser.TorrentResult import com.hippo.ehviewer.client.parser.VoteCommentResult import com.hippo.ehviewer.client.parser.VoteTagParser -import com.hippo.ehviewer.cronet.awaitBodyFully -import com.hippo.ehviewer.cronet.cronetRequest -import com.hippo.ehviewer.cronet.execute -import com.hippo.ehviewer.cronet.withRequestBody import com.hippo.ehviewer.dailycheck.showEventNotification import com.hippo.ehviewer.dailycheck.today import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.StatusCodeException -import com.hippo.ehviewer.util.isCronetSupported import eu.kanade.tachiyomi.util.system.logcat +import io.ktor.client.request.forms.append import io.ktor.client.statement.HttpStatement import io.ktor.client.statement.bodyAsChannel import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.request +import io.ktor.http.ContentType +import io.ktor.utils.io.core.writeFully import io.ktor.utils.io.pool.DirectByteBufferPool import io.ktor.utils.io.pool.useInstance import java.io.File @@ -75,18 +72,12 @@ import kotlinx.serialization.json.addJsonArray import kotlinx.serialization.json.put import moe.tarsin.coroutines.removeAllSuspend import moe.tarsin.coroutines.runSuspendCatching -import okhttp3.MediaType -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.asRequestBody import org.jsoup.Jsoup import splitties.init.appCtx const val TAG = "EhEngine" private const val MAX_REQUEST_SIZE = 25 private const val U_CONFIG_TEXT = "Selected Profile" -private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType() fun rethrowExactly(code: Int, body: Either, e: Throwable): Nothing { // Don't translate coroutine cancellation @@ -140,56 +131,6 @@ fun rethrowExactly(code: Int, body: Either, e: Throwable): N val httpContentPool = DirectByteBufferPool(8, 0x80000) -suspend fun fetchCompat( - url: String, - referer: String? = null, - origin: String? = null, - reqBody: RequestBody? = null, - parser: suspend (ByteBuffer) -> T, -): T { - Log.d(TAG, url) - fun commonChecks(body: ByteBuffer) { - // Check sad panda(without panda) - if (!body.hasRemaining()) { - if (EhUtils.isExHentai) { - throw EhException("Sad Panda\n(without panda)") - } else { - throw EhException("IP banned") - } - } - } - return if (isCronetSupported) { - cronetRequest(url, referer, origin) { - reqBody?.let { withRequestBody(it) } - }.execute { - @Suppress("NewApi") - val code = it.httpStatusCode - if (code >= 400) { - throw StatusCodeException(code) - } - httpContentPool.useInstance { buffer -> - awaitBodyFully(buffer) - buffer.flip() - commonChecks(buffer) - parser(buffer) - } - } - } else { - ehRequest(url, referer, origin) { - reqBody?.let { post(it) } - }.execute { - httpContentPool.useInstance { buffer -> - while (true) { - if (body.source().read(buffer) == -1) break - } - buffer.flip() - commonChecks(buffer) - parser(buffer) - } - } - } -} - suspend inline fun HttpStatement.parseString(crossinline block: suspend String.() -> T) = execute { response -> logcat(tag = TAG) { response.request.url.toString() } val body = response.bodyAsText() @@ -215,12 +156,9 @@ suspend inline fun HttpStatement.parseByteBuffer(crossinline block: suspend object EhEngine { suspend fun getOriginalImageUrl(url: String, referer: String?): String { - Log.d(TAG, url) - return ehRequest(url, referer).executeNoRedirect { - header("Location")?.apply { - if (contains("bounce_login")) { - throw NotLoggedInException() - } + return noRedirectStatement(url, referer).execute { + it.headers["Location"]?.apply { + if (contains("bounce_login")) throw NotLoggedInException() } ?: throw InsufficientFundsException() } } @@ -521,19 +459,16 @@ object EhEngine { /** * @param image Must be jpeg */ - suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = fetchCompat( - EhUrl.imageSearchUrl, - EhUrl.referer, - EhUrl.origin, + suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = statement(EhUrl.imageSearchUrl, EhUrl.referer, EhUrl.origin) { multipartBody { - setType(MultipartBody.FORM) - addFormDataPart("sfile", "a.jpg", image.asRequestBody(MEDIA_TYPE_JPEG)) - if (uss) addFormDataPart("fs_similar", "on") - if (osc) addFormDataPart("fs_covers", "on") - addFormDataPart("f_sfile", "File Search") - }, - parser = GalleryListParser::parse, - ).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl) } + append("sfile", "a.jpg", ContentType.Image.JPEG, image.length()) { + writeFully(image.readBytes()) + } + if (uss) append("fs_similar", "on") + if (osc) append("fs_covers", "on") + append("f_sfile", "File Search") + } + }.parseByteBuffer(GalleryListParser::parse).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl) } private suspend fun fillGalleryList(list: MutableList, url: String, filter: Boolean = false) { if (filter) list.removeAllSuspend { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt index 025acdbaa..e12183383 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhRequestBuilder.kt @@ -16,58 +16,38 @@ package com.hippo.ehviewer.client import com.hippo.ehviewer.EhApplication.Companion.ktorClient -import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient -import com.hippo.ehviewer.EhApplication.Companion.okHttpClient +import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient import com.hippo.ehviewer.Settings import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.forms.FormBuilder import io.ktor.client.request.forms.FormDataContent +import io.ktor.client.request.forms.MultiPartFormDataContent +import io.ktor.client.request.forms.formData import io.ktor.client.request.header import io.ktor.client.request.prepareRequest import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.HttpStatement +import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.http.ParametersBuilder import io.ktor.http.content.TextContent import io.ktor.http.userAgent -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArrayBuilder import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.okio.decodeFromBufferedSource -import okhttp3.MultipartBody -import okhttp3.Request -import okhttp3.Response -import okhttp3.executeAsync import okio.BufferedSource -inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply { - addHeader("User-Agent", Settings.userAgent) - addHeader("Accept", CHROME_ACCEPT) - addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE) - referer?.let { addHeader("Referer", it) } - origin?.let { addHeader("Origin", it) } -}.apply(builder).build() - -inline fun multipartBody(builder: MultipartBody.Builder.() -> Unit) = MultipartBody.Builder().apply(builder).build() - inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder)) -suspend inline fun Request.execute(block: Response.() -> R): R { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - return okHttpClient.newCall(this).executeAsync().use(block) -} +suspend inline fun HttpStatement.executeAndParseAs() = execute { it.parseAs() } -suspend inline fun Request.executeNoRedirect(block: Response.() -> R) = noRedirectOkHttpClient.newCall(this).executeAsync().use(block) - -suspend inline fun Request.executeAndParseAs() = execute { parseAs() } - -inline fun Response.parseAs(): T = body.source().parseAs() +suspend inline fun HttpResponse.parseAs(): T = bodyAsText().parseAs() inline fun BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this) inline fun String.parseAs(): T = json.decodeFromString(this) @@ -77,18 +57,32 @@ const val CHROME_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/53 const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5" -suspend inline fun statement( - url: String, - referer: String? = null, - origin: String? = null, - builder: HttpRequestBuilder.() -> Unit = {}, -) = ktorClient.prepareRequest(url) { +fun HttpRequestBuilder.applyEhConfig(referer: String?, origin: String?) { method = HttpMethod.Get header("Referer", referer) header("Origin", origin) userAgent(Settings.userAgent) header(HttpHeaders.Accept, CHROME_ACCEPT) header(HttpHeaders.AcceptLanguage, CHROME_ACCEPT_LANGUAGE) +} + +suspend inline fun statement( + url: String, + referer: String? = null, + origin: String? = null, + builder: HttpRequestBuilder.() -> Unit = {}, +) = ktorClient.prepareRequest(url) { + applyEhConfig(referer, origin) + apply(builder) +} + +suspend inline fun noRedirectStatement( + url: String, + referer: String? = null, + origin: String? = null, + builder: HttpRequestBuilder.() -> Unit = {}, +) = noRedirectKtorClient.prepareRequest(url) { + applyEhConfig(referer, origin) apply(builder) } @@ -103,3 +97,8 @@ fun HttpRequestBuilder.jsonBody(builder: JsonObjectBuilder.() -> Unit) { val json = buildJsonObject(builder).toString() setBody(TextContent(text = json, contentType = ContentType.Application.Json)) } + +fun HttpRequestBuilder.multipartBody(builder: FormBuilder.() -> Unit) { + method = HttpMethod.Post + setBody(MultiPartFormDataContent(formData(builder))) +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt index 4f6946ff2..f89f4a0da 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt @@ -19,11 +19,18 @@ import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient +import com.hippo.ehviewer.EhApplication.Companion.ktorClient import com.hippo.ehviewer.R import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.FileUtils -import com.hippo.ehviewer.util.copyToFile +import com.hippo.ehviewer.util.StatusCodeException +import io.ktor.client.HttpClient +import io.ktor.client.request.prepareGet +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.isSuccess +import io.ktor.util.cio.writeChannel +import io.ktor.utils.io.close +import io.ktor.utils.io.copyTo import java.io.File import java.nio.charset.StandardCharsets import kotlinx.coroutines.CoroutineScope @@ -35,9 +42,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.tarsin.coroutines.runSuspendCatching -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.executeAsync import okio.BufferedSource import okio.HashingSink.Companion.sha1 import okio.blackholeSink @@ -175,24 +179,23 @@ object EhTagDatabase : CoroutineScope { return sha1 != null && sha1 == getFileSha1(data) } - private suspend fun save(client: OkHttpClient, url: String, file: File): Boolean { - val request: Request = Request.Builder().url(url).build() - val call = client.newCall(request) + private suspend fun save(client: HttpClient, url: String, file: File) { runCatching { - call.executeAsync().use { response -> - if (!response.isSuccessful) { - return false + client.prepareGet(url).execute { + if (it.status.isSuccess()) { + val channel = file.writeChannel() + try { + it.bodyAsChannel().copyTo(channel) + } finally { + channel.close() + } + } else { + throw StatusCodeException(it.status.value) } - response.body.use { - it.copyToFile(file) - } - return true } }.onFailure { file.delete() - it.printStackTrace() - } - return false + }.getOrThrow() } suspend fun update() { @@ -246,7 +249,7 @@ object EhTagDatabase : CoroutineScope { // Save new sha1 val tempSha1File = File(dir, "$sha1Name.tmp") - check(save(baseOkHttpClient, sha1Url, tempSha1File)) + save(ktorClient, sha1Url, tempSha1File) val tempSha1 = getFileContent(tempSha1File) // Check new sha1 and current sha1 @@ -258,7 +261,7 @@ object EhTagDatabase : CoroutineScope { // Save new data val tempDataFile = File(dir, "$dataName.tmp") - check(save(baseOkHttpClient, dataUrl, tempDataFile)) + save(ktorClient, dataUrl, tempDataFile) // Check new sha1 and new data if (!checkData(tempSha1, tempDataFile)) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt b/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt index 86c4acd7a..402232910 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt @@ -1,16 +1,17 @@ package com.hippo.ehviewer.cronet -import com.hippo.ehviewer.EhApplication import io.ktor.utils.io.pool.DirectByteBufferPool import io.ktor.utils.io.pool.useInstance import java.nio.ByteBuffer import java.nio.channels.FileChannel import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.suspendCancellableCoroutine val pool = DirectByteBufferPool(32) -val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService +val cronetHttpClientExecutor = Dispatchers.IO.asExecutor() @Suppress("NewApi") suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/Cookie.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/Cookie.kt deleted file mode 100644 index fb97b0cc8..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/Cookie.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.hippo.ehviewer.dao - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "OK_HTTP_3_COOKIE") -data class Cookie( - @ColumnInfo(name = "NAME") - var name: String, - @ColumnInfo(name = "VALUE") - var value: String, - @ColumnInfo(name = "EXPIRES_AT") - var expiresAt: Long, - @ColumnInfo(name = "DOMAIN") - var domain: String, - @ColumnInfo(name = "PATH") - var path: String, - @ColumnInfo(name = "SECURE") - var secure: Boolean, - @ColumnInfo(name = "HTTP_ONLY") - var httpOnly: Boolean, - @ColumnInfo(name = "PERSISTENT") - var persistent: Boolean, - @ColumnInfo(name = "HOST_ONLY") - var hostOnly: Boolean, - @PrimaryKey - @ColumnInfo(name = "_id") - var id: Long?, -) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/CookiesDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/CookiesDao.kt deleted file mode 100644 index 5919119b4..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/CookiesDao.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.hippo.ehviewer.dao - -import androidx.room.Dao -import androidx.room.Query - -@Dao -interface CookiesDao { - @Query("SELECT * FROM OK_HTTP_3_COOKIE") - suspend fun list(): List -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt index 867128ad0..535280f9e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt @@ -181,21 +181,6 @@ abstract class EhDatabase : RoomDatabase() { abstract fun quickSearchDao(): QuickSearchDao } -// 1 -> 2 some nullability changes -@Database( - entities = [Cookie::class], - version = 2, - autoMigrations = [ - AutoMigration( - from = 1, - to = 2, - ), - ], -) -abstract class CookiesDatabase : RoomDatabase() { - abstract fun cookiesDao(): CookiesDao -} - @Database( entities = [Search::class], version = 2, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Okhttp.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Okhttp.kt index ac8d46393..68e82e952 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Okhttp.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Okhttp.kt @@ -1,10 +1,6 @@ package com.hippo.ehviewer.ktbuilder -import okhttp3.Cache import okhttp3.OkHttpClient -import okio.FileSystem -import okio.Path inline fun httpClient(builder: OkHttpClient.Builder.() -> Unit): OkHttpClient = OkHttpClient.Builder().apply(builder).build() inline fun httpClient(client: OkHttpClient, builder: OkHttpClient.Builder.() -> Unit): OkHttpClient = client.newBuilder().apply(builder).build() -fun OkHttpClient.Builder.cache(directory: Path, maxSize: Long, fileSystem: FileSystem = FileSystem.SYSTEM) = cache(Cache(directory, maxSize, fileSystem)) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/legacy/CookiesMigration.kt b/app/src/main/kotlin/com/hippo/ehviewer/legacy/CookiesMigration.kt deleted file mode 100644 index 586ef554d..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/legacy/CookiesMigration.kt +++ /dev/null @@ -1,38 +0,0 @@ -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 LEGACY_COOKIES_DB = "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 (LEGACY_COOKIES_DB !in appCtx.databaseList()) return - resource { - roomDb(LEGACY_COOKIES_DB) - } release { - it.close() - appCtx.deleteDatabase(LEGACY_COOKIES_DB) - } use { database -> - database.cookiesDao().list().forEach { - EhCookieStore.addCookie(it.toOkHttp3Cookie()) - } - EhCookieStore.flush() - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/CookieSignInScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/CookieSignInScreen.kt index be1dcc996..2b21c5824 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/CookieSignInScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/CookieSignInScreen.kt @@ -68,7 +68,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext import java.util.Locale import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import okhttp3.Cookie @Destination @Composable @@ -93,15 +92,13 @@ fun CookieSignInScene(navigator: DestinationsNavigator) { val noCookies = stringResource(R.string.from_clipboard_error) - fun newCookie(name: String, value: String, domain: String) = Cookie.Builder().name(name).value(value).domain(domain).expiresAt(Long.MAX_VALUE).build() - fun storeCookie(id: String, hash: String, igneous: String) { EhUtils.signOut() - EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_E)) - EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_EX)) - EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E)) - EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX)) - if (igneous.isNotBlank()) EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX)) + EhCookieStore.addCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_E) + EhCookieStore.addCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_EX) + EhCookieStore.addCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E) + EhCookieStore.addCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX) + if (igneous.isNotBlank()) EhCookieStore.addCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX) } fun login() { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/WebViewSignInScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/WebViewSignInScreen.kt index 41b6944be..f385354b3 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/WebViewSignInScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/WebViewSignInScreen.kt @@ -22,7 +22,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import eu.kanade.tachiyomi.util.lang.withUIContext -import okhttp3.HttpUrl.Companion.toHttpUrl +import io.ktor.http.Url @Destination @Composable @@ -39,7 +39,7 @@ fun WebViewSignInScreen(navigator: DestinationsNavigator) { } var getId = false var getHash = false - EhCookieStore.loadForRequest(EhUrl.HOST_E.toHttpUrl()).forEach { + EhCookieStore.load(Url(EhUrl.HOST_E)).forEach { if (EhCookieStore.KEY_IPB_MEMBER_ID == it.name) { getId = true } else if (EhCookieStore.KEY_IPB_PASS_HASH == it.name) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt index 6122adc9d..34fe8f1ab 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt @@ -170,7 +170,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.parcelize.Parcelize import moe.tarsin.coroutines.runSuspendCatching -import okhttp3.HttpUrl.Companion.toHttpUrl import splitties.systemservices.downloadManager import splitties.systemservices.layoutInflater @@ -673,7 +672,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa FileUtils.sanitizeFilename("$name.torrent"), ) r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - r.addRequestHeader("Cookie", EhCookieStore.getCookieHeader(itemUrl.toHttpUrl())) + r.addRequestHeader("Cookie", EhCookieStore.getCookieHeader(itemUrl)) try { downloadManager.enqueue(r) activity.showTip(R.string.download_torrent_started) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt index cdbcdd96d..694a03542 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt @@ -54,11 +54,11 @@ import com.hippo.ehviewer.util.copyTextToClipboard import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.withUIContext +import io.ktor.http.Url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch import moe.tarsin.coroutines.runSuspendCatching -import okhttp3.HttpUrl.Companion.toHttpUrl @Destination @Composable @@ -91,8 +91,8 @@ fun EhScreen(navigator: DestinationsNavigator) { title = stringResource(id = R.string.account_name), summary = Settings.displayName ?: touristMode, ) { - val eCookies = EhCookieStore.loadForRequest(EhUrl.HOST_E.toHttpUrl()) - val exCookies = EhCookieStore.loadForRequest(EhUrl.HOST_EX.toHttpUrl()) + val eCookies = EhCookieStore.load(Url(EhUrl.HOST_E)) + val exCookies = EhCookieStore.load(Url(EhUrl.HOST_EX)) var ipbMemberId: String? = null var ipbPassHash: String? = null var igneous: String? = null diff --git a/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt b/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt index 455c1e9b4..ab57a321f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/updater/AppUpdater.kt @@ -1,9 +1,17 @@ package com.hippo.ehviewer.updater import com.hippo.ehviewer.BuildConfig +import com.hippo.ehviewer.EhApplication.Companion.ktorClient import com.hippo.ehviewer.Settings -import com.hippo.ehviewer.client.execute import com.hippo.ehviewer.client.executeAndParseAs +import io.ktor.client.request.header +import io.ktor.client.request.prepareGet +import io.ktor.client.statement.bodyAsChannel +import io.ktor.client.statement.bodyAsText +import io.ktor.util.cio.writeChannel +import io.ktor.utils.io.close +import io.ktor.utils.io.copyTo +import io.ktor.utils.io.jvm.javaio.toInputStream import java.io.File import java.time.Instant import java.time.temporal.ChronoUnit @@ -11,8 +19,6 @@ import java.util.zip.ZipInputStream import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi import moe.tarsin.coroutines.runSuspendCatching -import okhttp3.Request -import okio.sink import org.json.JSONObject import tachiyomi.data.release.GithubArtifacts import tachiyomi.data.release.GithubCommitComparison @@ -31,18 +37,16 @@ object AppUpdater { Settings.lastUpdateDay = now.epochSecond if (Settings.useCIUpdateChannel) { val curSha = BuildConfig.COMMIT_SHA - val branch = ghRequest(API_URL).execute { - JSONObject(body.string()).getString("default_branch") - } + val branch = ghStatement(API_URL).execute { JSONObject(it.bodyAsText()).getString("default_branch") } val workflowRunsUrl = "$API_URL/actions/workflows/ci.yml/runs?branch=$branch&event=push&status=success&per_page=1" - val workflowRun = ghRequest(workflowRunsUrl).executeAndParseAs().workflowRuns[0] + val workflowRun = ghStatement(workflowRunsUrl).executeAndParseAs().workflowRuns[0] val shortSha = workflowRun.headSha.take(7) if (shortSha != curSha) { - val artifacts = ghRequest(workflowRun.artifactsUrl).executeAndParseAs() + val artifacts = ghStatement(workflowRun.artifactsUrl).executeAndParseAs() val archiveUrl = artifacts.getDownloadLink() val changelog = runSuspendCatching { val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha" - val result = ghRequest(commitComparisonUrl).executeAndParseAs() + val result = ghStatement(commitComparisonUrl).executeAndParseAs() // TODO: Prettier format, Markdown? result.commits.joinToString("\n") { commit -> "${commit.commit.message.takeWhile { it != '\n' }} (@${commit.author.name})" @@ -52,7 +56,7 @@ object AppUpdater { } } else { val curVersion = BuildConfig.VERSION_NAME.substringBefore('-') - val release = ghRequest(LATEST_RELEASE_URL).executeAndParseAs() + val release = ghStatement(LATEST_RELEASE_URL).executeAndParseAs() val latestVersion = release.version val description = release.info val downloadUrl = release.getDownloadLink() @@ -65,29 +69,34 @@ object AppUpdater { } suspend fun downloadUpdate(url: String, file: File) = - ghRequest(url).execute { + ghStatement(url).execute { response -> if (url.endsWith("zip")) { - ZipInputStream(body.byteStream()).use { zip -> - zip.nextEntry - file.outputStream().use { - zip.copyTo(it) + response.bodyAsChannel().toInputStream().use { stream -> + ZipInputStream(stream).use { zip -> + zip.nextEntry + file.outputStream().use { + zip.copyTo(it) + } } } } else { - file.sink().use { - body.source().readAll(it) + val channel = file.writeChannel() + try { + response.bodyAsChannel().copyTo(channel) + } finally { + channel.close() } } } } @OptIn(ExperimentalEncodingApi::class) -private inline fun ghRequest(url: String, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply { +private suspend inline fun ghStatement(url: String) = ktorClient.prepareGet(url) { val token = "github_" + "pat_11AXZS" + "T4A0k3TArCGakP3t_7DzUE5S" + "mr1zw8rmmzVtCeRq62" + "A4qkuDMw6YQm5ZUtHSLZ2MLI3J4VSifLXZ" val user = "nullArrayList" val base64 = Base64.encode("$user:$token".toByteArray()) - addHeader("Authorization", "Basic $base64") -}.apply(builder).build() + header("Authorization", "Basic $base64") +} data class Release( val version: String, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/IOUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/IOUtils.kt deleted file mode 100644 index 7420bee82..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/IOUtils.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.hippo.ehviewer.util - -import java.io.File -import okhttp3.ResponseBody -import okio.buffer -import okio.sink - -fun ResponseBody.copyToFile(file: File) { - file.outputStream().use { os -> - // Prior to the adoption of OpenJDK, transferFrom will call ByteBuffer.allocate((int) count) - if (isAtLeastN) { - os.channel.transferFrom(source(), 0, Long.MAX_VALUE) - } else { - os.sink().buffer().use { it.writeAll(source()) } - } - } -} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 6d0b2cf1e..8157191d1 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -8,8 +8,8 @@ import android.webkit.WebViewClient import androidx.core.content.ContextCompat import com.hippo.ehviewer.client.EhCookieStore import com.hippo.ehviewer.client.exception.CloudflareBypassException +import io.ktor.http.Url import java.util.concurrent.CountDownLatch -import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response @@ -52,8 +52,7 @@ class CloudflareInterceptor(context: Context) : WebViewInterceptor(context) { webview?.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { fun isCloudFlareBypassed(): Boolean { - return EhCookieStore.get(origRequestUrl.toHttpUrl()) - .any { it.name == COOKIE_NAME } + return EhCookieStore.load(Url(origRequestUrl)).any { it.name == COOKIE_NAME } } if (isCloudFlareBypassed()) { diff --git a/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt b/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt index 872a08f27..5ab2befaa 100644 --- a/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt +++ b/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt @@ -21,7 +21,6 @@ import kotlin.coroutines.resumeWithException import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.RequestBody import okio.Buffer import okio.Path.Companion.toOkioPath @@ -70,7 +69,7 @@ class CronetRequest { inline fun cronetRequest(url: String, referer: String? = null, origin: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply { request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply { - addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl())) + addHeader("Cookie", EhCookieStore.getCookieHeader(url)) addHeader("Accept", CHROME_ACCEPT) addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE) referer?.let { addHeader("Referer", it) }