mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Add universal captcha solver
This commit is contained in:
		@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user