Captcha support.

This commit is contained in:
Koen 2023-10-17 13:17:54 +02:00
parent f49ecf1159
commit 851b547d64
13 changed files with 273 additions and 15 deletions

View file

@ -127,6 +127,10 @@
android:name=".activities.ExceptionActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.CaptchaActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.LoginActivity"
android:screenOrientation="portrait"

View file

@ -64,6 +64,14 @@ class ScriptException extends Error {
}
}
}
class CaptchaRequiredException extends Error {
constructor(url, body) {
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
this.plugin_type = "CaptchaRequiredException";
this.url = url;
this.body = body;
}
}
class UnavailableException extends ScriptException {
constructor(msg) {
super("UnavailableException", msg);

View file

@ -0,0 +1,118 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebView
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.CaptchaWebViewClient
import com.futo.platformplayer.others.LoginWebViewClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
import java.lang.Exception
import java.util.UUID
class CaptchaActivity : AppCompatActivity() {
private lateinit var _webView: WebView;
private lateinit var _buttonClose: Button;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_captcha);
setNavigationBarColorAndIcons();
_buttonClose = findViewById(R.id.button_close);
_buttonClose.setOnClickListener { finish(); };
_webView = findViewById(R.id.web_view);
_webView.settings.javaScriptEnabled = true;
CookieManager.getInstance().setAcceptCookie(true);
val url = if (intent.hasExtra("url"))
intent.getStringExtra("url");
else null;
if (url == null) {
throw Exception("URL is missing");
}
val body = if (intent.hasExtra("body"))
intent.getStringExtra("body");
else null;
if (body == null) {
throw Exception("Body is missing");
}
_webView.settings.useWideViewPort = true;
_webView.settings.loadWithOverviewMode = true;
val webViewClient = CaptchaWebViewClient();
webViewClient.onCaptchaFinished.subscribe { googleAbuseCookie ->
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));
}
}
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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"));
}
}
}

View file

@ -108,11 +108,12 @@ class PackageHttp: V8Package {
}
@kotlinx.serialization.Serializable
class BridgeHttpResponse(val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = 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);
}
}

View file

@ -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<ScriptCaptchaRequiredException> {
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<ScriptExecutionException> {
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<ScriptImplementationException> {
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<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
);
}
.exception<Throwable> {
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()
}) {

View file

@ -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<String>();
val onPageLoaded = Event2<WebView?, String?>()
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";
}
}

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/black">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:layout_gravity="center_vertical"
android:text="Please enter the captcha and close when finished" />
<Button
android:id="@+id/button_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="6dp"
android:text="CLOSE" />
</LinearLayout>
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

@ -1 +1 @@
Subproject commit 123960682a286232963b5ed456598b1922dbe559
Subproject commit 40c9307c06e0005a972fe4c94c3c89a421379e0d

@ -1 +1 @@
Subproject commit 75816961722eb8166866f96f7003d1f5e9d59e8e
Subproject commit 40c9307c06e0005a972fe4c94c3c89a421379e0d