SpyFakku: Add mirror selection to preferences page (#11373)

* Add mirror selection to SpyFakku

* Move this to resolve null pointer

* mirror pref + trust certs for airdns domain + anibus bypass for airdns domain

* inspector crash

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
anewadventure 2025-11-22 05:24:33 +07:00 committed by Draff
parent 709609fb70
commit fdc8e29671
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 195 additions and 17 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'SpyFakku'
extClass = '.SpyFakku'
extVersionCode = 11
extVersionCode = 12
isNsfw = true
}

View File

@ -0,0 +1,99 @@
package eu.kanade.tachiyomi.extension.en.spyfakku
import android.annotation.SuppressLint
import android.app.Application
import android.net.http.SslError
import android.os.Handler
import android.os.Looper
import android.webkit.CookieManager
import android.webkit.SslErrorHandler
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
object AnibusInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
return if (request.url.host.contains("airdns")) {
val document = Jsoup.parse(
response.peekBody(Long.MAX_VALUE).string(),
request.url.toString(),
)
if (document.selectFirst("script#anubis_challenge") != null) {
response.close()
if (!resolveInWebView(request)) {
throw IOException("Failed to resolve challenge in WebView")
} else {
chain.proceed(request)
}
} else {
response
}
} else {
response
}
}
@Synchronized
@SuppressLint("SetJavaScriptEnabled")
private fun resolveInWebView(request: Request): Boolean {
val context = Injekt.get<Application>()
val cookieManager = CookieManager.getInstance()
val latch = CountDownLatch(1)
var webView: WebView? = null
val handler = Handler(Looper.getMainLooper())
handler.post {
val webview = WebView(context)
.also { webView = it }
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
userAgentString = request.header("User-Agent")
}
webview.webViewClient = object : WebViewClient() {
@SuppressLint("WebViewClientOnReceivedSslError")
override fun onReceivedSslError(
view: WebView,
handler: SslErrorHandler,
error: SslError,
) {
handler.proceed()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
val cookie = cookieManager.getCookie(url)
.split("; ").map { it.split("=", limit = 2) }
val auth = cookie.firstOrNull { it.first().contains("anubis-auth") && it.last().isNotBlank() }
if (auth != null) {
latch.countDown()
}
}
}
webview.loadUrl(request.url.toString())
}
latch.await(20, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
}
return latch.count != 1L
}
}

View File

@ -1,7 +1,11 @@
package eu.kanade.tachiyomi.extension.en.spyfakku
import android.annotation.SuppressLint
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -9,6 +13,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferences
import keiyoushi.utils.tryParse
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@ -23,38 +29,95 @@ import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import kotlin.random.Random
class SpyFakku : HttpSource() {
class SpyFakku : HttpSource(), ConfigurableSource {
override val name = "SpyFakku"
override val baseUrl = "https://hentalk.pw"
private val baseImageUrl = "$baseUrl/image"
private val baseApiUrl = "$baseUrl/api"
override val lang = "en"
override val supportsLatest = true
private val preferences = getPreferences()
override val baseUrl: String
get() {
val index = preferences.getString(MIRROR_PREF_KEY, "0")!!.toInt()
.coerceAtMost(mirrors.size - 1)
return mirrors[index]
}
private val baseImageUrl = "$tmpCdnUrl/image"
private val baseApiUrl get() = "$baseUrl/api"
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
// add referer in interceptor due to domain change preference
.addNetworkInterceptor { chain ->
val request = chain.request().newBuilder()
.header("Referer", "$baseUrl/")
.header("Origin", baseUrl)
.build()
chain.proceed(request)
}
// change domain of image urls
.addInterceptor { chain ->
val url = chain.request().url
if (url.host == tmpCdnDomain) {
val (host, port) = baseUrl.toHttpUrl().let { it.host to it.port }
val newUrl = url.newBuilder()
.scheme("https")
.host(host)
.port(port)
.build()
val request = chain.request().newBuilder()
.url(newUrl)
.build()
chain.proceed(request)
} else {
chain.proceed(chain.request())
}
}
// airdns domain is self-signed
.apply {
val naiveTrustManager = @SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate?> = emptyArray()
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) = Unit
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) = Unit
}
val insecureSocketFactory = SSLContext.getInstance("SSL").apply {
val trustAllCerts = arrayOf<TrustManager>(naiveTrustManager)
init(null, trustAllCerts, SecureRandom())
}.socketFactory
sslSocketFactory(insecureSocketFactory, naiveTrustManager)
hostnameVerifier { _, _ -> true }
}
// airdns domain is protected by Anibus
.addInterceptor(AnibusInterceptor)
.rateLimit(2, 1, TimeUnit.SECONDS)
.build()
private val charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
.set("Origin", baseUrl)
override fun popularMangaRequest(page: Int): Request {
return GET("$baseApiUrl/library?sort=released_at&page=$page", headers)
}
@ -287,11 +350,7 @@ class SpyFakku : HttpSource() {
SChapter.create().apply {
name = "Chapter"
url = manga.url
date_upload = try {
releasedAtFormat.parse(add.released_at)!!.time
} catch (e: Exception) {
0L
}
date_upload = releasedAtFormat.tryParse(add.released_at)
},
),
)
@ -362,4 +421,24 @@ class SpyFakku : HttpSource() {
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = MIRROR_PREF_KEY
title = "Preferred Mirror"
entries = mirrors
entryValues = Array(mirrors.size) { it.toString() }
summary = "%s"
setDefaultValue("0")
}.also(screen::addPreference)
}
}
private const val MIRROR_PREF_KEY = "pref_mirror"
private val mirrors = arrayOf(
"https://hentalk.pw",
"https://fakku.cc",
"https://fakkuonion.airdns.org:4096",
)
private const val tmpCdnDomain = "127.0.0.1"
private const val tmpCdnUrl = "http://$tmpCdnDomain"