Skip to content

Commit

Permalink
Added 0ms.dev images proxy support #771
Browse files Browse the repository at this point in the history
  • Loading branch information
Koitharu committed May 20, 2024
1 parent 6055776 commit 745972a
Show file tree
Hide file tree
Showing 17 changed files with 249 additions and 122 deletions.
2 changes: 1 addition & 1 deletion app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.network

enum class DoHProvider {

NONE, GOOGLE, CLOUDFLARE, ADGUARD
}
NONE, GOOGLE, CLOUDFLARE, ADGUARD, ZERO_MS
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,6 +31,9 @@ interface NetworkModule {
@Binds
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar

@Binds
fun bindImageProxyInterceptor(impl: RealImageProxyInterceptor): ImageProxyInterceptor

companion object {

@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>())

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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading

0 comments on commit 745972a

Please sign in to comment.