From 851b547d6403cdb8f87f455d3929ba040b36ec41 Mon Sep 17 00:00:00 2001 From: Koen Date: Tue, 17 Oct 2023 13:17:54 +0200 Subject: [PATCH] Captcha support. --- app/src/main/AndroidManifest.xml | 4 + app/src/main/assets/scripts/source.js | 8 ++ .../activities/CaptchaActivity.kt | 118 ++++++++++++++++++ .../platforms/js/internal/JSHttpClient.kt | 10 ++ .../developer/DeveloperEndpoints.kt | 2 +- .../futo/platformplayer/engine/V8Plugin.kt | 15 ++- .../ScriptCaptchaRequiredException.kt | 16 +++ .../engine/packages/PackageHttp.kt | 15 +-- .../mainactivity/main/HomeFragment.kt | 23 +++- .../others/CaptchaWebViewClient.kt | 38 ++++++ app/src/main/res/layout/activity_captcha.xml | 35 ++++++ app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 13 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/activities/CaptchaActivity.kt create mode 100644 app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt create mode 100644 app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt create mode 100644 app/src/main/res/layout/activity_captcha.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2abba88c..fb7e0590 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -127,6 +127,10 @@ android:name=".activities.ExceptionActivity" android:screenOrientation="portrait" android:theme="@style/Theme.FutoVideo.NoActionBar" /> + + Logger.i(TAG, "Abuse cookie found: $googleAbuseCookie"); + _callback?.let { + _callback = null; + it.invoke(googleAbuseCookie); + } + finish(); + }; + _webView.settings.domStorageEnabled = true; + _webView.webViewClient = webViewClient; + _webView.loadDataWithBaseURL(url, body, "text/html", "utf-8", null); + //_webView.loadUrl(url); + } + + override fun finish() { + lifecycleScope.launch(Dispatchers.Main) { + _webView.loadUrl("about:blank"); + } + _callback?.let { + _callback = null; + it.invoke(null); + } + super.finish(); + } + + companion object { + private val TAG = "CaptchaActivity"; + private var _callback: ((String?) -> Unit)? = null; + + private fun getCaptchaIntent(context: Context, url: String, body: String): Intent { + val intent = Intent(context, CaptchaActivity::class.java); + intent.putExtra("url", url); + intent.putExtra("body", body); + return intent; + } + + fun showCaptcha(context: Context, url: String, body: String, callback: ((String?) -> Unit)? = null) { + val cookieManager = CookieManager.getInstance(); + val cookieString = cookieManager.getCookie("https://youtube.com") + val cookieMap = cookieString.split(";") + .map { it.trim() } + .map { it.split("=", limit = 2) } + .filter { it.size == 2 } + .associate { it[0] to it[1] }; + + if (cookieMap.containsKey("GOOGLE_ABUSE_EXEMPTION")) { + callback?.invoke("GOOGLE_ABUSE_EXEMPTION=" + cookieMap["GOOGLE_ABUSE_EXEMPTION"]); + return; + } + + _callback = callback; + context.startActivity(getCaptchaIntent(context, url, body)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt index 6f1e3b1b..7eb77988 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt @@ -67,6 +67,12 @@ class JSHttpClient : ManagedHttpClient { } } + if (exemptionId != null) { + val cookie = request.headers["Cookie"]; + request.headers["Cookie"] = (cookie ?: "") + ";$exemptionId" + Logger.i(TAG, "Exemption ID applied: ${request.headers["Cookie"]}") + } + _jsClient?.validateUrlOrThrow(request.url); super.beforeRequest(request) } @@ -155,4 +161,8 @@ class JSHttpClient : ManagedHttpClient { Logger.i("Testing", code); } + + companion object { + var exemptionId: String? = null; + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt b/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt index 808662b9..c8e1e471 100644 --- a/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt +++ b/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt @@ -416,7 +416,7 @@ class DeveloperEndpoints(private val context: Context) { val resp = _client.get(body.url!!, body.headers); context.respondCode(200, - Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.code, resp.body?.string())), + Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())), context.query.getOrDefault("CT", "text/plain")); } catch(ex: Exception) { diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index a3508dfd..0aa74623 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -259,18 +259,27 @@ class V8Plugin { throw ScriptCompilationException(config, "Compilation: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped); } catch(executeEx: JavetExecutionException) { - val exMessage = extractJSExceptionMessage(executeEx); + if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) { + val pluginType = executeEx.scriptingError.context["plugin_type"].toString(); + if (pluginType == "CaptchaRequiredException") { + throw ScriptCaptchaRequiredException(config, + executeEx.scriptingError.context["url"].toString(), + executeEx.scriptingError.context["body"].toString(), + executeEx, executeEx.scriptingError?.stack, codeStripped) + }; - if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) + val exMessage = extractJSExceptionMessage(executeEx); throwExceptionFromV8( config, - executeEx.scriptingError.context["plugin_type"].toString(), + pluginType, (exMessage ?: ""), executeEx, executeEx.scriptingError?.stack, codeStripped ); + } + val exMessage = extractJSExceptionMessage(executeEx); throw ScriptExecutionException(config, "${exMessage}", null, executeEx.scriptingError?.stack, codeStripped); } catch(ex: Exception) { diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt new file mode 100644 index 00000000..41090556 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt @@ -0,0 +1,16 @@ +package com.futo.platformplayer.engine.exceptions + +import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.getOrThrow + +class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String, val body: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, "Captcha required", ex, stack, code) { + + companion object { + fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + return ScriptCaptchaRequiredException(config, + obj.getOrThrow(config, "url", "ScriptCaptchaRequiredException"), + obj.getOrThrow(config, "body", "ScriptCaptchaRequiredException")); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt index 36372af4..e35edfa8 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt @@ -108,11 +108,12 @@ class PackageHttp: V8Package { } @kotlinx.serialization.Serializable - class BridgeHttpResponse(val code: Int, val body: String?, val headers: Map>? = null) : IV8Convertable { + class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map>? = null) : IV8Convertable { val isOk = code >= 200 && code < 300; override fun toV8(runtime: V8Runtime): V8Value? { val obj = runtime.createV8ValueObject(); + obj.set("url", url); obj.set("code", code); obj.set("body", body); obj.set("headers", headers); @@ -227,7 +228,7 @@ class PackageHttp: V8Package { val resp = client.requestMethod(method, url, headers); val responseBody = resp.body?.string(); logResponse(method, url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); } }; } @@ -241,7 +242,7 @@ class PackageHttp: V8Package { val resp = client.requestMethod(method, url, body, headers); val responseBody = resp.body?.string(); logResponse(method, url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); } }; } @@ -256,7 +257,7 @@ class PackageHttp: V8Package { val resp = client.get(url, headers); val responseBody = resp.body?.string(); logResponse("GET", url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); } }; } @@ -270,7 +271,7 @@ class PackageHttp: V8Package { val resp = client.post(url, body, headers); val responseBody = resp.body?.string(); logResponse("POST", url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); } }; } @@ -367,7 +368,7 @@ class PackageHttp: V8Package { } //Forward timeouts catch(ex: SocketTimeoutException) { - return BridgeHttpResponse(408, null); + return BridgeHttpResponse("", 408, null); } } } @@ -461,7 +462,7 @@ class PackageHttp: V8Package { } //Forward timeouts catch(ex: SocketTimeoutException) { - return BridgeHttpResponse(408, null); + return BridgeHttpResponse("", 408, null); } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index 2ecbbca2..3bfab4c9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -8,21 +8,26 @@ import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.futo.platformplayer.* +import com.futo.platformplayer.activities.CaptchaActivity import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.platforms.js.JSClient +import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.others.CaptchaWebViewClient import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.views.announcements.AnnouncementView import com.futo.platformplayer.views.FeedStyle @@ -93,6 +98,20 @@ class HomeFragment : MainFragment() { StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope) }) .success { loadedResult(it); } + .exception { + Logger.w(TAG, "Plugin captcha required.", it); + + UIDialogs.showConfirmationDialog(context, "Captcha required\nPlugin [${it.config.name}]", action = { + CaptchaActivity.showCaptcha(context, it.url, it.body) { + if (it != null) { + Logger.i(TAG, "Captcha entered $it") + JSHttpClient.exemptionId = it; + //TODO: Reload plugin when captcha completed? is it necessary + loadResults(); + } + } + }) + } .exception { Logger.w(ChannelFragment.TAG, "Plugin failure.", it); UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0, @@ -101,14 +120,14 @@ class HomeFragment : MainFragment() { ); } .exception { - Logger.w(ChannelFragment.TAG, "Plugin failure.", it); + Logger.w(TAG, "Plugin failure.", it); UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0, UIDialogs.Action("Ignore", {}), UIDialogs.Action("Sources", { fragment.navigate() }, UIDialogs.ActionStyle.PRIMARY) ); } .exception { - Logger.w(ChannelFragment.TAG, "Failed to load channel.", it); + Logger.w(TAG, "Failed to load channel.", it); UIDialogs.showGeneralRetryErrorDialog(context, "Failed to get Home", it, { loadResults() }) { diff --git a/app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt b/app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt new file mode 100644 index 00000000..04f1df5e --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt @@ -0,0 +1,38 @@ +package com.futo.platformplayer.others + +import android.webkit.* +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.constructs.Event2 +import com.futo.platformplayer.logging.Logger + +class CaptchaWebViewClient : WebViewClient { + val onCaptchaFinished = Event1(); + val onPageLoaded = Event2() + + constructor() : super() {} + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url); + Logger.i(TAG, "onPageFinished url = ${url}") + onPageLoaded.emit(view, url); + } + + override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? { + if(request == null) + return super.shouldInterceptRequest(view, request as WebResourceRequest?); + + Logger.i(TAG, "shouldInterceptRequest url = ${request.url}") + if (request.url.isHierarchical) { + val googleAbuse = request.url.getQueryParameter("google_abuse"); + if (googleAbuse != null) { + onCaptchaFinished.emit(googleAbuse); + } + } + + return super.shouldInterceptRequest(view, request); + } + + companion object { + private val TAG = "CaptchaWebViewClient"; + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_captcha.xml b/app/src/main/res/layout/activity_captcha.xml new file mode 100644 index 00000000..051f346b --- /dev/null +++ b/app/src/main/res/layout/activity_captcha.xml @@ -0,0 +1,35 @@ + + + + + + + +