diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index 0c38a0f50..de03baa97 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -27,8 +27,8 @@ import okhttp3.OkHttpClient import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHManager.kt index 0cc4b6db0..6e82fe9c1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHManager.kt @@ -83,6 +83,11 @@ class DoHManager( tryGetByIp("2a10:50c0::2:ff"), ), ).build() + + DoHProvider.ZERO_MS -> DnsOverHttps.Builder().client(bootstrapClient) + .url("https://0ms.dev/dns-query".toHttpUrl()) + .resolvePublicAddresses(true) + .build() } private fun tryGetByIp(ip: String): InetAddress? = try { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHProvider.kt index e17db70a7..b898fd8c1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHProvider.kt @@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.network enum class DoHProvider { - NONE, GOOGLE, CLOUDFLARE, ADGUARD -} \ No newline at end of file + NONE, GOOGLE, CLOUDFLARE, ADGUARD, ZERO_MS +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ImageProxyInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ImageProxyInterceptor.kt deleted file mode 100644 index 6aca9bee5..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ImageProxyInterceptor.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.koitharu.kotatsu.core.network - -import android.util.Log -import androidx.collection.ArraySet -import coil.intercept.Interceptor -import coil.request.ErrorResult -import coil.request.ImageResult -import coil.request.SuccessResult -import coil.size.Dimension -import coil.size.isOriginal -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.util.ext.ensureSuccess -import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps -import org.koitharu.kotatsu.parsers.util.await -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import java.util.Collections -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ImageProxyInterceptor @Inject constructor( - private val settings: AppSettings, -) : Interceptor { - - private val blacklist = Collections.synchronizedSet(ArraySet()) - - override suspend fun intercept(chain: Interceptor.Chain): ImageResult { - val request = chain.request - if (!settings.isImagesProxyEnabled) { - return chain.proceed(request) - } - val url: HttpUrl? = when (val data = request.data) { - is HttpUrl -> data - is String -> data.toHttpUrlOrNull() - else -> null - } - if (url == null || !url.isHttpOrHttps || url.host in blacklist) { - return chain.proceed(request) - } - val newUrl = HttpUrl.Builder() - .scheme("https") - .host("wsrv.nl") - .addQueryParameter("url", url.toString()) - .addQueryParameter("we", null) - val size = request.sizeResolver.size() - if (!size.isOriginal) { - newUrl.addQueryParameter("crop", "cover") - (size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) } - (size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) } - } - - val newRequest = request.newBuilder() - .data(newUrl.build()) - .build() - val result = chain.proceed(newRequest) - return if (result is SuccessResult) { - result - } else { - logDebug((result as? ErrorResult)?.throwable) - chain.proceed(request).also { - if (it is SuccessResult) { - blacklist.add(url.host) - } - } - } - } - - suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response { - if (!settings.isImagesProxyEnabled) { - return okHttp.newCall(request).await() - } - val sourceUrl = request.url - val targetUrl = HttpUrl.Builder() - .scheme("https") - .host("wsrv.nl") - .addQueryParameter("url", sourceUrl.toString()) - .addQueryParameter("we", null) - val newRequest = request.newBuilder() - .url(targetUrl.build()) - .build() - return runCatchingCancellable { - okHttp.doCall(newRequest) - }.recover { - logDebug(it) - okHttp.doCall(request).also { - blacklist.add(sourceUrl.host) - } - }.getOrThrow() - } - - private suspend fun OkHttpClient.doCall(request: Request): Response { - return newCall(request).await().ensureSuccess() - } - - private fun logDebug(e: Throwable?) { - if (BuildConfig.DEBUG) { - Log.w("ImageProxy", e.toString()) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt index 8e0d80ea8..a89e7c162 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -15,6 +15,8 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar +import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor +import org.koitharu.kotatsu.core.network.imageproxy.RealImageProxyInterceptor import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread import org.koitharu.kotatsu.local.data.LocalStorageManager @@ -29,6 +31,9 @@ interface NetworkModule { @Binds fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar + @Binds + fun bindImageProxyInterceptor(impl: RealImageProxyInterceptor): ImageProxyInterceptor + companion object { @Provides diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/BaseImageProxyInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/BaseImageProxyInterceptor.kt new file mode 100644 index 000000000..b88966367 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/BaseImageProxyInterceptor.kt @@ -0,0 +1,87 @@ +package org.koitharu.kotatsu.core.network.imageproxy + +import android.util.Log +import androidx.collection.ArraySet +import coil.intercept.Interceptor +import coil.network.HttpException +import coil.request.ErrorResult +import coil.request.ImageRequest +import coil.request.ImageResult +import coil.request.SuccessResult +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.HttpStatusException +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException +import org.koitharu.kotatsu.core.util.ext.ensureSuccess +import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps +import org.koitharu.kotatsu.parsers.util.await +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import java.net.HttpURLConnection +import java.util.Collections + +abstract class BaseImageProxyInterceptor : ImageProxyInterceptor { + + private val blacklist = Collections.synchronizedSet(ArraySet()) + + final override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val request = chain.request + val url: HttpUrl? = when (val data = request.data) { + is HttpUrl -> data + is String -> data.toHttpUrlOrNull() + else -> null + } + if (url == null || !url.isHttpOrHttps || url.host in blacklist) { + return chain.proceed(request) + } + val newRequest = onInterceptImageRequest(request, url) + return when (val result = chain.proceed(newRequest)) { + is SuccessResult -> result + is ErrorResult -> { + logDebug(result.throwable, newRequest.data) + chain.proceed(request).also { + if (it is SuccessResult && result.throwable.isBlockedByServer()) { + blacklist.add(url.host) + } + } + } + } + } + + final override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response { + val newRequest = onInterceptPageRequest(request) + return runCatchingCancellable { + okHttp.doCall(newRequest) + }.recover { error -> + logDebug(error, newRequest.url) + okHttp.doCall(request).also { + if (error.isBlockedByServer()) { + blacklist.add(request.url.host) + } + } + }.getOrThrow() + } + + protected abstract suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest + + protected abstract suspend fun onInterceptPageRequest(request: Request): Request + + private suspend fun OkHttpClient.doCall(request: Request): Response { + return newCall(request).await().ensureSuccess() + } + + private fun logDebug(e: Throwable, url: Any) { + if (BuildConfig.DEBUG) { + Log.w("ImageProxy", "${e.message}: $url", e) + } + } + + private fun Throwable.isBlockedByServer(): Boolean { + return this is CloudFlareBlockedException + || (this is HttpException && response.code == HttpURLConnection.HTTP_FORBIDDEN) + || (this is HttpStatusException && statusCode == HttpURLConnection.HTTP_FORBIDDEN) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/ImageProxyInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/ImageProxyInterceptor.kt new file mode 100644 index 000000000..7b5e4b1bc --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/ImageProxyInterceptor.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.core.network.imageproxy + +import coil.intercept.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response + +interface ImageProxyInterceptor : Interceptor { + + suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/RealImageProxyInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/RealImageProxyInterceptor.kt new file mode 100644 index 000000000..2ade36803 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/RealImageProxyInterceptor.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.core.network.imageproxy + +import coil.intercept.Interceptor +import coil.request.ImageResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.plus +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import org.koitharu.kotatsu.parsers.util.await +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RealImageProxyInterceptor @Inject constructor( + private val settings: AppSettings, +) : ImageProxyInterceptor { + + private val delegate = settings.observeAsStateFlow( + scope = processLifecycleScope + Dispatchers.Default, + key = AppSettings.KEY_IMAGES_PROXY, + valueProducer = { createDelegate() }, + ) + + override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + return delegate.value?.intercept(chain) ?: chain.proceed(chain.request) + } + + override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response { + return delegate.value?.interceptPageRequest(request, okHttp) ?: okHttp.newCall(request).await() + } + + private fun createDelegate(): ImageProxyInterceptor? = when (val proxy = settings.imagesProxy) { + -1 -> null + 0 -> WsrvNlProxyInterceptor() + 1 -> ZeroMsProxyInterceptor() + else -> error("Unsupported images proxy $proxy") + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/WsrvNlProxyInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/WsrvNlProxyInterceptor.kt new file mode 100644 index 000000000..2645bf78c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/WsrvNlProxyInterceptor.kt @@ -0,0 +1,40 @@ +package org.koitharu.kotatsu.core.network.imageproxy + +import coil.request.ImageRequest +import coil.size.Dimension +import coil.size.isOriginal +import okhttp3.HttpUrl +import okhttp3.Request + +class WsrvNlProxyInterceptor : BaseImageProxyInterceptor() { + + override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest { + val newUrl = HttpUrl.Builder() + .scheme("https") + .host("wsrv.nl") + .addQueryParameter("url", url.toString()) + .addQueryParameter("we", null) + val size = request.sizeResolver.size() + if (!size.isOriginal) { + newUrl.addQueryParameter("crop", "cover") + (size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) } + (size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) } + } + + return request.newBuilder() + .data(newUrl.build()) + .build() + } + + override suspend fun onInterceptPageRequest(request: Request): Request { + val sourceUrl = request.url + val targetUrl = HttpUrl.Builder() + .scheme("https") + .host("wsrv.nl") + .addQueryParameter("url", sourceUrl.toString()) + .addQueryParameter("we", null) + return request.newBuilder() + .url(targetUrl.build()) + .build() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/ZeroMsProxyInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/ZeroMsProxyInterceptor.kt new file mode 100644 index 000000000..ba7670d67 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/imageproxy/ZeroMsProxyInterceptor.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.core.network.imageproxy + +import coil.request.ImageRequest +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request + +class ZeroMsProxyInterceptor : BaseImageProxyInterceptor() { + + override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest { + if (url.host == "x.0ms.dev" || url.host == "0ms.dev") { + return request + } + val newUrl = ("https://x.0ms.dev/q70/$url").toHttpUrl() + return request.newBuilder() + .data(newUrl) + .build() + } + + override suspend fun onInterceptPageRequest(request: Request): Request { + val newUrl = ("https://x.0ms.dev/q70/${request.url}").toHttpUrl() + return request.newBuilder() + .url(newUrl) + .build() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 9a1c128e3..dccb102b9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -380,8 +380,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { } } - val isImagesProxyEnabled: Boolean - get() = prefs.getBoolean(KEY_IMAGES_PROXY, false) + val imagesProxy: Int + get() { + val raw = prefs.getString(KEY_IMAGES_PROXY, null)?.toIntOrNull() + return raw ?: if (prefs.getBoolean(KEY_IMAGES_PROXY_OLD, false)) 0 else -1 + } val dnsOverHttps: DoHProvider get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE) @@ -548,8 +551,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { companion object { - const val PAGE_SWITCH_VOLUME_KEYS = "volume" - const val TRACK_HISTORY = "history" const val TRACK_FAVOURITES = "favourites" @@ -663,7 +664,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_PROXY_AUTH = "proxy_auth" const val KEY_PROXY_LOGIN = "proxy_login" const val KEY_PROXY_PASSWORD = "proxy_password" - const val KEY_IMAGES_PROXY = "images_proxy" + const val KEY_IMAGES_PROXY = "images_proxy_2" const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs" const val KEY_DISABLE_NSFW = "no_nsfw" const val KEY_RELATED_MANGA = "related_manga" @@ -689,5 +690,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_APP_TRANSLATION = "about_app_translation" const val KEY_FEED_HEADER = "feed_header" const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types" + + // old keys are for migration only + private const val KEY_IMAGES_PROXY_OLD = "images_proxy" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt index 494b0305a..3bfe70c52 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt @@ -19,8 +19,8 @@ import okhttp3.OkHttpClient import okio.Path.Companion.toOkioPath import okio.buffer import okio.source -import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.isFileUri diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt index f5eae4021..cb7ec0917 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -7,8 +7,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okhttp3.OkHttpClient import org.koitharu.kotatsu.core.model.findChapter -import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 013b0e883..ec6e41640 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -27,8 +27,8 @@ import okhttp3.OkHttpClient import okhttp3.Request import okio.use import org.koitharu.kotatsu.core.network.CommonHeaders -import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 3f037e11a..7acd5f705 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -35,6 +35,12 @@ Google CloudFlare AdGuard + 0ms + + + @string/none + wsrv.nl + 0ms.dev @string/standard diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 7ef06238f..77dd0dc98 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -40,6 +40,11 @@ 2 0 + + -1 + 0 + 1 + @string/sync_host_default moe.shirizu.org diff --git a/app/src/main/res/xml/pref_network.xml b/app/src/main/res/xml/pref_network.xml index d32293566..919eefd89 100644 --- a/app/src/main/res/xml/pref_network.xml +++ b/app/src/main/res/xml/pref_network.xml @@ -33,11 +33,13 @@ android:title="@string/dns_over_https" app:useSimpleSummaryProvider="true" /> - +