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
import android.app.Application
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.*
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import eu.kanade.tachiyomi.util.asJsoup
import exh.ui.captcha.SolveCaptchaActivity
import okhttp3.*
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.net.URI
@ -65,7 +68,37 @@ abstract class HttpSource : CatalogueSource {
* Default network client for doing requests.
*/
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.

View File

@ -290,7 +290,8 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
}
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)
.addNetworkInterceptor {
val cAspNetCookie = preferences.eh_ts_aspNetCookie().getOrDefault()

View File

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

View File

@ -14,7 +14,8 @@ import java.nio.charset.Charset
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class AutoSolvingWebViewClient(activity: SolveCaptchaActivity,
source: CaptchaCompletionVerifier,
injectScript: String?)
injectScript: String?,
private val headers: Map<String, String>)
: BasicWebViewClient(activity, source, injectScript) {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
@ -31,6 +32,24 @@ class AutoSolvingWebViewClient(activity: SolveCaptchaActivity,
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)
}
}

View File

@ -26,17 +26,18 @@ import android.os.SystemClock
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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 rx.Observable
class SolveCaptchaActivity : AppCompatActivity() {
private val sourceManager: SourceManager by injectLazy()
private val preferencesHelper: PreferencesHelper by injectLazy()
private val networkHelper: NetworkHelper by injectLazy()
val httpClient = OkHttpClient.Builder()
.maybeInjectEHLogger()
.build()
val httpClient = networkHelper.client
private val jsonParser = JsonParser()
private var currentLoopId: String? = null
@ -51,9 +52,19 @@ class SolveCaptchaActivity : AppCompatActivity() {
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
val source = if(sourceId != -1L)
sourceManager.get(sourceId) as? CaptchaCompletionVerifier
else null
val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null
val source = if(originalSource != 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>?
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
@ -62,7 +73,7 @@ class SolveCaptchaActivity : AppCompatActivity() {
val url: String? = intent.getStringExtra(URL_EXTRA)
if(source == null || cookies == null || url == null) {
if(source == null || url == null) {
finish()
return
}
@ -73,80 +84,71 @@ class SolveCaptchaActivity : AppCompatActivity() {
val cm = CookieManager.getInstance()
fun continueLoading() {
cookies.forEach { (t, u) ->
val cookieString = t + "=" + u + "; domain=" + parsedUrl.host
cm.setCookie(url, cookieString)
}
cookies?.forEach { (t, u) ->
val cookieString = t + "=" + u + "; domain=" + parsedUrl.host
cm.setCookie(url, cookieString)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
CookieSyncManager.createInstance(this).sync()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
CookieSyncManager.createInstance(this).sync()
webview.settings.javaScriptEnabled = true
webview.settings.domStorageEnabled = true
webview.settings.javaScriptEnabled = true
webview.settings.domStorageEnabled = true
var loadedInners = 0
var loadedInners = 0
webview.webChromeClient = object : WebChromeClient() {
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
if(message.startsWith("exh-")) {
loadedInners++
// Wait for both inner scripts to be loaded
if(loadedInners >= 2) {
// Attempt to autosolve captcha
if(preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webview.post {
// 10 seconds to auto-solve captcha
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
beginSolveLoop()
beginValidateCaptchaLoop()
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE) {
webview.evaluateJavascript(SOLVE_UI_SCRIPT_SHOW, null)
}
webview.webChromeClient = object : WebChromeClient() {
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
if(message.startsWith("exh-")) {
loadedInners++
// Wait for both inner scripts to be loaded
if(loadedInners >= 2) {
// Attempt to autosolve captcha
if(preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webview.post {
// 10 seconds to auto-solve captcha
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
beginSolveLoop()
beginValidateCaptchaLoop()
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE) {
webview.evaluateJavascript(SOLVE_UI_SCRIPT_SHOW, null)
}
}
}
result.confirm()
return true
}
return false
result.confirm()
return true
}
return false
}
webview.webViewClient = if (preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Fetch auto-solve credentials early for speed
credentialsObservable = httpClient.newCall(Request.Builder()
// Rob demo credentials
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
.build())
.asObservableSuccess()
.subscribeOn(Schedulers.io())
.map {
val json = jsonParser.parse(it.body()!!.string())
it.close()
json["token"].string
}.melt()
webview.addJavascriptInterface(this@SolveCaptchaActivity, "exh")
AutoSolvingWebViewClient(this, source, script)
} else {
BasicWebViewClient(this, source, script)
}
webview.loadUrl(url)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cm.removeAllCookies { continueLoading() }
webview.webViewClient = if (preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Fetch auto-solve credentials early for speed
credentialsObservable = httpClient.newCall(Request.Builder()
// Rob demo credentials
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
.build())
.asObservableSuccess()
.subscribeOn(Schedulers.io())
.map {
val json = jsonParser.parse(it.body()!!.string())
it.close()
json["token"].string
}.melt()
webview.addJavascriptInterface(this@SolveCaptchaActivity, "exh")
AutoSolvingWebViewClient(this, source, script, headers)
} else {
cm.removeAllCookie()
continueLoading()
BasicWebViewClient(this, source, script)
}
webview.loadUrl(url, headers)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
@ -611,9 +613,28 @@ class SolveCaptchaActivity : AppCompatActivity() {
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 {
fun verifyNoCaptcha(url: String): Boolean
}