mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Captcha support.
This commit is contained in:
parent
f49ecf1159
commit
851b547d64
13 changed files with 273 additions and 15 deletions
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}) {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
35
app/src/main/res/layout/activity_captcha.xml
Normal file
35
app/src/main/res/layout/activity_captcha.xml
Normal 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
|
Loading…
Add table
Reference in a new issue