Add universal captcha solver

This commit is contained in:
NerdNumber9 2019-04-18 20:38:04 -04:00
parent 852c1a423d
commit ec4af65c36
5 changed files with 147 additions and 75 deletions

View File

@ -1,16 +1,19 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import android.app.Application
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import okhttp3.Headers import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient import exh.ui.captcha.SolveCaptchaActivity
import okhttp3.Request import okhttp3.*
import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.lang.Exception import java.lang.Exception
import java.net.URI import java.net.URI
@ -65,7 +68,37 @@ abstract class HttpSource : CatalogueSource {
* Default network client for doing requests. * Default network client for doing requests.
*/ */
open val client: OkHttpClient open val client: OkHttpClient
get() = network.client get() = network.client.newBuilder().addInterceptor { chain ->
val response = chain.proceed(chain.request())
val body = response.body()
if(!response.isSuccessful && body != null) {
if(body.contentType()?.type() == "text"
&& body.contentType()?.subtype() == "html") {
val bodyString = body.string()
val rebuiltResponse = response.newBuilder()
.body(ResponseBody.create(body.contentType(), bodyString))
.build()
try {
// Search for captcha
val parsed = response.asJsoup(html = bodyString)
if(parsed.getElementsByClass("g-recaptcha").isNotEmpty()) {
// Found it, allow the user to solve this thing
SolveCaptchaActivity.launchUniversal(
Injekt.get<Application>(),
this,
chain.request().url().toString()
)
}
} catch(t: Throwable) {
// Ignore all errors
XLog.w("Captcha detection error!", t)
}
return@addInterceptor rebuiltResponse
}
}
response
}.build()
/** /**
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.

View File

@ -290,7 +290,8 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
} }
override val client: OkHttpClient override val client: OkHttpClient
get() = super.client.newBuilder() // Do not call super here as we don't want auto-captcha detection here
get() = network.client.newBuilder()
.cookieJar(CookieJar.NO_COOKIES) .cookieJar(CookieJar.NO_COOKIES)
.addNetworkInterceptor { .addNetworkInterceptor {
val cAspNetCookie = preferences.eh_ts_aspNetCookie().getOrDefault() val cAspNetCookie = preferences.eh_ts_aspNetCookie().getOrDefault()

View File

@ -2,9 +2,7 @@ package exh.eh
import android.support.v4.util.AtomicFile import android.support.v4.util.AtomicFile
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseIntArray
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import exh.ui.captcha.SolveCaptchaActivity.Companion.launch
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock

View File

@ -14,7 +14,8 @@ import java.nio.charset.Charset
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class AutoSolvingWebViewClient(activity: SolveCaptchaActivity, class AutoSolvingWebViewClient(activity: SolveCaptchaActivity,
source: CaptchaCompletionVerifier, source: CaptchaCompletionVerifier,
injectScript: String?) injectScript: String?,
private val headers: Map<String, String>)
: BasicWebViewClient(activity, source, injectScript) { : BasicWebViewClient(activity, source, injectScript) {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
@ -31,6 +32,24 @@ class AutoSolvingWebViewClient(activity: SolveCaptchaActivity,
doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered() doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered()
) )
} }
if(headers.isNotEmpty()) {
val response = activity.httpClient.newCall(request.toOkHttpRequest()
.newBuilder()
.apply {
headers.forEach { (n, v) -> addHeader(n, v) }
}
.build())
.execute()
return WebResourceResponse(
response.body()?.contentType()?.let { "${it.type()}/${it.subtype()}" },
response.body()?.contentType()?.charset()?.toString(),
response.code(),
response.message(),
response.headers().toMultimap().mapValues { it.value.joinToString(",") },
response.body()?.byteStream()
)
}
return super.shouldInterceptRequest(view, request) return super.shouldInterceptRequest(view, request)
} }
} }

View File

@ -26,17 +26,18 @@ import android.os.SystemClock
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import exh.log.maybeInjectEHLogger import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource
import exh.source.DelegatedHttpSource
import exh.util.melt import exh.util.melt
import rx.Observable import rx.Observable
class SolveCaptchaActivity : AppCompatActivity() { class SolveCaptchaActivity : AppCompatActivity() {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val preferencesHelper: PreferencesHelper by injectLazy() private val preferencesHelper: PreferencesHelper by injectLazy()
private val networkHelper: NetworkHelper by injectLazy()
val httpClient = OkHttpClient.Builder() val httpClient = networkHelper.client
.maybeInjectEHLogger()
.build()
private val jsonParser = JsonParser() private val jsonParser = JsonParser()
private var currentLoopId: String? = null private var currentLoopId: String? = null
@ -51,9 +52,19 @@ class SolveCaptchaActivity : AppCompatActivity() {
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha) setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1) val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
val source = if(sourceId != -1L) val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null
sourceManager.get(sourceId) as? CaptchaCompletionVerifier val source = if(originalSource != null) {
else null originalSource as? CaptchaCompletionVerifier
?: run {
(originalSource as? HttpSource)?.let {
NoopCaptchaCompletionVerifier(it)
}
}
} else null
val headers = (source as? HttpSource)?.headers?.toMultimap()?.mapValues {
it.value.joinToString(",")
} ?: emptyMap()
val cookies: HashMap<String, String>? val cookies: HashMap<String, String>?
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String> = intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
@ -62,7 +73,7 @@ class SolveCaptchaActivity : AppCompatActivity() {
val url: String? = intent.getStringExtra(URL_EXTRA) val url: String? = intent.getStringExtra(URL_EXTRA)
if(source == null || cookies == null || url == null) { if(source == null || url == null) {
finish() finish()
return return
} }
@ -73,8 +84,7 @@ class SolveCaptchaActivity : AppCompatActivity() {
val cm = CookieManager.getInstance() val cm = CookieManager.getInstance()
fun continueLoading() { cookies?.forEach { (t, u) ->
cookies.forEach { (t, u) ->
val cookieString = t + "=" + u + "; domain=" + parsedUrl.host val cookieString = t + "=" + u + "; domain=" + parsedUrl.host
cm.setCookie(url, cookieString) cm.setCookie(url, cookieString)
} }
@ -130,20 +140,12 @@ class SolveCaptchaActivity : AppCompatActivity() {
}.melt() }.melt()
webview.addJavascriptInterface(this@SolveCaptchaActivity, "exh") webview.addJavascriptInterface(this@SolveCaptchaActivity, "exh")
AutoSolvingWebViewClient(this, source, script) AutoSolvingWebViewClient(this, source, script, headers)
} else { } else {
BasicWebViewClient(this, source, script) BasicWebViewClient(this, source, script)
} }
webview.loadUrl(url) webview.loadUrl(url, headers)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cm.removeAllCookies { continueLoading() }
} else {
cm.removeAllCookie()
continueLoading()
}
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
@ -611,7 +613,26 @@ class SolveCaptchaActivity : AppCompatActivity() {
context.startActivity(intent) context.startActivity(intent)
} }
fun launchUniversal(context: Context,
source: HttpSource,
url: String) {
val intent = Intent(context, SolveCaptchaActivity::class.java).apply {
putExtra(SOURCE_ID_EXTRA, source.id)
putExtra(URL_EXTRA, url)
} }
context.startActivity(intent)
}
}
}
class NoopCaptchaCompletionVerifier(private val source: HttpSource): DelegatedHttpSource(source),
CaptchaCompletionVerifier {
override val versionId get() = source.versionId
override val lang: String get() = source.lang
override fun verifyNoCaptcha(url: String) = false
} }
interface CaptchaCompletionVerifier : Source { interface CaptchaCompletionVerifier : Source {