Add universal captcha solver
This commit is contained in:
parent
852c1a423d
commit
ec4af65c36
@ -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.
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user