Captcha plugin system

This commit is contained in:
Kelvin
2023-10-17 15:25:46 +02:00
parent 851b547d64
commit df0504cead
19 changed files with 423 additions and 121 deletions
@@ -10,7 +10,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginCaptchaConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@@ -41,38 +43,48 @@ class CaptchaActivity : AppCompatActivity() {
_webView.settings.javaScriptEnabled = true; _webView.settings.javaScriptEnabled = true;
CookieManager.getInstance().setAcceptCookie(true); CookieManager.getInstance().setAcceptCookie(true);
val url = if (intent.hasExtra("url"))
val config = if(intent.hasExtra("plugin"))
Json.decodeFromString<SourcePluginConfig>(intent.getStringExtra("plugin")!!);
else null;
val captchaConfig = if(config != null)
config.captcha ?: throw IllegalStateException("Plugin has no captcha support");
else if(intent.hasExtra("captcha"))
Json.decodeFromString<SourcePluginCaptchaConfig>(intent.getStringExtra("captcha")!!);
else throw IllegalStateException("No valid configuration?");
//TODO: Backwards compat removal?
val extraUrl = if (intent.hasExtra("url"))
intent.getStringExtra("url"); intent.getStringExtra("url");
else null; else null;
if (url == null) { val extraBody = if (intent.hasExtra("body"))
throw Exception("URL is missing");
}
val body = if (intent.hasExtra("body"))
intent.getStringExtra("body"); intent.getStringExtra("body");
else null; else null;
if (body == null) { _webView.settings.userAgentString = captchaConfig.userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36";
throw Exception("Body is missing");
}
_webView.settings.useWideViewPort = true; _webView.settings.useWideViewPort = true;
_webView.settings.loadWithOverviewMode = true; _webView.settings.loadWithOverviewMode = true;
val webViewClient = CaptchaWebViewClient(); val webViewClient = if(config != null) CaptchaWebViewClient(config) else CaptchaWebViewClient(captchaConfig);
webViewClient.onCaptchaFinished.subscribe { googleAbuseCookie -> webViewClient.onCaptchaFinished.subscribe { captcha ->
Logger.i(TAG, "Abuse cookie found: $googleAbuseCookie");
_callback?.let { _callback?.let {
_callback = null; _callback = null;
it.invoke(googleAbuseCookie); it.invoke(captcha);
} }
finish(); finish();
}; };
_webView.settings.domStorageEnabled = true; _webView.settings.domStorageEnabled = true;
_webView.webViewClient = webViewClient; _webView.webViewClient = webViewClient;
_webView.loadDataWithBaseURL(url, body, "text/html", "utf-8", null);
//_webView.loadUrl(url); if(captchaConfig.captchaUrl != null)
_webView.loadUrl(captchaConfig.captchaUrl);
else if(extraUrl != null && extraBody != null)
_webView.loadDataWithBaseURL(extraUrl, extraBody, "text/html", "utf-8", null);
else if(extraUrl != null)
_webView.loadUrl(extraUrl);
else throw IllegalStateException("No valid captcha info provided");
} }
override fun finish() { override fun finish() {
@@ -88,31 +100,21 @@ class CaptchaActivity : AppCompatActivity() {
companion object { companion object {
private val TAG = "CaptchaActivity"; private val TAG = "CaptchaActivity";
private var _callback: ((String?) -> Unit)? = null; private var _callback: ((SourceCaptchaData?) -> Unit)? = null;
private fun getCaptchaIntent(context: Context, url: String, body: String): Intent { private fun getCaptchaIntent(context: Context, config: SourcePluginConfig, url: String? = null, body: String? = null): Intent {
val intent = Intent(context, CaptchaActivity::class.java); val intent = Intent(context, CaptchaActivity::class.java);
intent.putExtra("url", url); if(url != null)
intent.putExtra("body", body); intent.putExtra("url", url);
if(body != null)
intent.putExtra("body", body);
intent.putExtra("plugin", Json.encodeToString(config));
return intent; return intent;
} }
fun showCaptcha(context: Context, url: String, body: String, callback: ((String?) -> Unit)? = null) { fun showCaptcha(context: Context, config: SourcePluginConfig, url: String? = null, body: String? = null, callback: ((SourceCaptchaData?) -> 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; _callback = callback;
context.startActivity(getCaptchaIntent(context, url, body)); context.startActivity(getCaptchaIntent(context, config, url, body));
} }
} }
} }
@@ -894,7 +894,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>(); private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
private var requestCode: Int? = -1; private var requestCode: Int? = -1;
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult( private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
@@ -15,29 +15,36 @@ class DevJSClient : JSClient {
private val _devScript: String; private val _devScript: String;
private var _auth: SourceAuth? = null; private var _auth: SourceAuth? = null;
private var _captcha: SourceCaptchaData? = null;
val devID: String; val devID: String;
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), listOf("DEV")), null, script) { constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) {
_devScript = script; _devScript = script;
_auth = auth; _auth = auth;
_captcha = captcha;
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5); this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
} }
constructor(context: Context, descriptor: SourcePluginDescriptor, script: String, auth: SourceAuth? = null, savedState: String? = null, devID: String? = null): super(context, descriptor, savedState, script) { //TODO: Misisng auth/captcha pass on purpose?
constructor(context: Context, descriptor: SourcePluginDescriptor, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, savedState: String? = null, devID: String? = null): super(context, descriptor, savedState, script) {
_devScript = script; _devScript = script;
_auth = auth; _auth = auth;
_captcha = captcha;
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5); this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
} }
fun setCaptcha(captcha: SourceCaptchaData? = null) {
_captcha = captcha;
}
fun setAuth(auth: SourceAuth? = null) { fun setAuth(auth: SourceAuth? = null) {
_auth = auth; _auth = auth;
} }
fun recreate(context: Context): DevJSClient { fun recreate(context: Context): DevJSClient {
return DevJSClient(context, config, _devScript, _auth, devID); return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
} }
override fun getCopy(): JSClient { override fun getCopy(): JSClient {
return DevJSClient(_context, descriptor, _script, _auth, saveState(), devID); return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
} }
override fun initialize() { override fun initialize() {
@@ -25,9 +25,11 @@ import com.futo.platformplayer.api.media.platforms.js.internal.*
import com.futo.platformplayer.api.media.platforms.js.models.* import com.futo.platformplayer.api.media.platforms.js.models.*
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.PluginEngineException import com.futo.platformplayer.engine.exceptions.PluginEngineException
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptValidationException import com.futo.platformplayer.engine.exceptions.ScriptValidationException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@@ -61,6 +63,7 @@ open class JSClient : IPlatformClient {
private var _enabled: Boolean = false; private var _enabled: Boolean = false;
private val _auth: SourceAuth?; private val _auth: SourceAuth?;
private val _captcha: SourceCaptchaData?;
private val _injectedSaveState: String?; private val _injectedSaveState: String?;
@@ -87,6 +90,7 @@ open class JSClient : IPlatformClient {
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
val onDisabled = Event1<JSClient>(); val onDisabled = Event1<JSClient>();
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) { constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
this._context = context; this._context = context;
@@ -95,10 +99,11 @@ open class JSClient : IPlatformClient {
this.descriptor = descriptor; this.descriptor = descriptor;
_injectedSaveState = saveState; _injectedSaveState = saveState;
_auth = descriptor.getAuth(); _auth = descriptor.getAuth();
_captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
_client = JSHttpClient(this); _client = JSHttpClient(this, null, _captcha);
_clientAuth = JSHttpClient(this, _auth); _clientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth); _plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
_plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js"); _plugin.withDependency(context, "scripts/source.js");
@@ -110,6 +115,11 @@ open class JSClient : IPlatformClient {
} }
else else
throw IllegalStateException("Script for plugin [${descriptor.config.name}] was not available"); throw IllegalStateException("Script for plugin [${descriptor.config.name}] was not available");
_plugin.onScriptException.subscribe {
if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it);
};
} }
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) { constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
this._context = context; this._context = context;
@@ -118,15 +128,21 @@ open class JSClient : IPlatformClient {
this.descriptor = descriptor; this.descriptor = descriptor;
_injectedSaveState = saveState; _injectedSaveState = saveState;
_auth = descriptor.getAuth(); _auth = descriptor.getAuth();
_captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
_client = JSHttpClient(this); _client = JSHttpClient(this, null, _captcha);
_clientAuth = JSHttpClient(this, _auth); _clientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth); _plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
_plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js"); _plugin.withDependency(context, "scripts/source.js");
_plugin.withScript(script); _plugin.withScript(script);
_script = script; _script = script;
_plugin.onScriptException.subscribe {
if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it);
};
} }
open fun getCopy(): JSClient { open fun getCopy(): JSClient {
@@ -0,0 +1,49 @@
package com.futo.platformplayer.api.media.platforms.js
import com.futo.platformplayer.encryption.EncryptionProvider
import com.futo.platformplayer.logging.Logger
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
override fun toString(): String {
return "(headers: '$headers', cookieString: '$cookieMap')";
}
fun toEncrypted(): String{
return EncryptionProvider.instance.encrypt(serialize());
}
private fun serialize(): String {
return Json.encodeToString(SerializedCaptchaData(cookieMap, headers));
}
companion object {
val TAG = "SourceAuth";
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
if(encrypted == null)
return null;
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
try {
return deserialize(decrypted);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize authentication", ex);
return null;
}
}
fun deserialize(str: String): SourceCaptchaData {
val data = Json.decodeFromString<SerializedCaptchaData>(str);
return SourceCaptchaData(data.cookieMap, data.headers);
}
}
@Serializable
data class SerializedCaptchaData(val cookieMap: HashMap<String, HashMap<String, String>>?,
val headers: Map<String, Map<String, String>> = mapOf())
}
@@ -0,0 +1,12 @@
package com.futo.platformplayer.api.media.platforms.js
import kotlinx.serialization.Serializable
@Serializable
class SourcePluginCaptchaConfig(
val captchaUrl: String? = null,
val completionUrl: String? = null,
val cookiesToFind: List<String>? = null,
val userAgent: String? = null,
val cookiesExclOthers: Boolean = true
)
@@ -35,6 +35,7 @@ class SourcePluginConfig(
val settings: List<Setting> = listOf(), val settings: List<Setting> = listOf(),
var captcha: SourcePluginCaptchaConfig? = null,
val authentication: SourcePluginAuthConfig? = null, val authentication: SourcePluginAuthConfig? = null,
var sourceUrl: String? = null, var sourceUrl: String? = null,
val constants: HashMap<String, String> = hashMapOf(), val constants: HashMap<String, String> = hashMapOf(),
@@ -13,22 +13,28 @@ class SourcePluginDescriptor {
var appSettings: AppPluginSettings = AppPluginSettings(); var appSettings: AppPluginSettings = AppPluginSettings();
var authEncrypted: String? var authEncrypted: String? = null
private set;
var captchaEncrypted: String? = null
private set; private set;
val flags: List<String>; val flags: List<String>;
@kotlinx.serialization.Transient @kotlinx.serialization.Transient
val onAuthChanged = Event0(); val onAuthChanged = Event0();
@kotlinx.serialization.Transient
val onCaptchaChanged = Event0();
constructor(config :SourcePluginConfig, authEncrypted: String? = null) { constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) {
this.config = config; this.config = config;
this.authEncrypted = authEncrypted; this.authEncrypted = authEncrypted;
this.captchaEncrypted = captchaEncrypted;
this.flags = listOf(); this.flags = listOf();
} }
constructor(config :SourcePluginConfig, authEncrypted: String? = null, flags: List<String>) { constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) {
this.config = config; this.config = config;
this.authEncrypted = authEncrypted; this.authEncrypted = authEncrypted;
this.captchaEncrypted = captchaEncrypted;
this.flags = flags; this.flags = flags;
} }
@@ -41,6 +47,13 @@ class SourcePluginDescriptor {
return map; return map;
} }
fun updateCaptcha(captcha: SourceCaptchaData?) {
captchaEncrypted = captcha?.toEncrypted();
onCaptchaChanged.emit();
}
fun getCaptchaData(): SourceCaptchaData? {
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
}
fun updateAuth(str: SourceAuth?) { fun updateAuth(str: SourceAuth?) {
authEncrypted = str?.toEncrypted(); authEncrypted = str?.toEncrypted();
@@ -5,72 +5,73 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
class JSHttpClient : ManagedHttpClient { class JSHttpClient : ManagedHttpClient {
private val _jsClient: JSClient?; private val _jsClient: JSClient?;
private val _auth: SourceAuth?; private val _auth: SourceAuth?;
private val _captcha: SourceCaptchaData?;
var doUpdateCookies: Boolean = true; var doUpdateCookies: Boolean = true;
var doApplyCookies: Boolean = true; var doApplyCookies: Boolean = true;
var doAllowNewCookies: Boolean = true; var doAllowNewCookies: Boolean = true;
val isLoggedIn: Boolean get() = _auth != null; val isLoggedIn: Boolean get() = _auth != null;
private var _currentCookieMap: HashMap<String, HashMap<String, String>>?; private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
constructor(jsClient: JSClient?, auth: SourceAuth? = null) : super() { constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null) : super() {
_jsClient = jsClient; _jsClient = jsClient;
_auth = auth; _auth = auth;
_captcha = captcha;
_currentCookieMap = hashMapOf();
if(!auth?.cookieMap.isNullOrEmpty()) { if(!auth?.cookieMap.isNullOrEmpty()) {
_currentCookieMap = hashMapOf();
for(domainCookies in auth!!.cookieMap!!) for(domainCookies in auth!!.cookieMap!!)
_currentCookieMap!!.put(domainCookies.key, HashMap(domainCookies.value)); _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
} }
else _currentCookieMap = null; if(!captcha?.cookieMap.isNullOrEmpty()) {
for(domainCookies in auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
} }
override fun clone(): ManagedHttpClient { override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth); val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = if(_currentCookieMap != null) newClient._currentCookieMap = if(_currentCookieMap != null)
HashMap(_currentCookieMap!!.toList().associate { Pair(it.first, HashMap(it.second)) }) HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
else else
null; hashMapOf();
return newClient; return newClient;
} }
override fun beforeRequest(request: Request) { override fun beforeRequest(request: Request) {
val domain = Uri.parse(request.url).host!!.lowercase();
val auth = _auth; val auth = _auth;
if (auth != null) { if (auth != null) {
val domain = Uri.parse(request.url).host!!.lowercase();
//TODO: Possibly add doApplyHeaders //TODO: Possibly add doApplyHeaders
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries }) for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
request.headers[header.key] = header.value; request.headers[header.key] = header.value;
if(doApplyCookies) {
if (!_currentCookieMap.isNullOrEmpty()) {
val cookiesToApply = hashMapOf<String, String>();
synchronized(_currentCookieMap!!) {
for(cookie in _currentCookieMap!!
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
};
if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
request.headers["Cookie"] = cookieString;
}
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
}
}
} }
if (exemptionId != null) { if(doApplyCookies) {
val cookie = request.headers["Cookie"]; if (!_currentCookieMap.isNullOrEmpty()) {
request.headers["Cookie"] = (cookie ?: "") + ";$exemptionId" val cookiesToApply = hashMapOf<String, String>();
Logger.i(TAG, "Exemption ID applied: ${request.headers["Cookie"]}") synchronized(_currentCookieMap!!) {
for(cookie in _currentCookieMap!!
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
};
if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
request.headers["Cookie"] = cookieString;
}
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
}
} }
_jsClient?.validateUrlOrThrow(request.url); _jsClient?.validateUrlOrThrow(request.url);
@@ -86,7 +87,7 @@ class JSHttpClient : ManagedHttpClient {
val defaultCookieDomain = val defaultCookieDomain =
"." + domainParts.drop(domainParts.size - 2).joinToString("."); "." + domainParts.drop(domainParts.size - 2).joinToString(".");
for (header in resp.headers) { for (header in resp.headers) {
if (_currentCookieMap != null && header.key.lowercase() == "set-cookie") { if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.key.lowercase() == "set-cookie") {
val newCookies = cookieStringToMap(header.value); val newCookies = cookieStringToMap(header.value);
for (cookie in newCookies) { for (cookie in newCookies) {
val endIndex = cookie.value.indexOf(";"); val endIndex = cookie.value.indexOf(";");
@@ -162,7 +163,4 @@ class JSHttpClient : ManagedHttpClient {
Logger.i("Testing", code); Logger.i("Testing", code);
} }
companion object {
var exemptionId: String? = null;
}
} }
@@ -51,6 +51,8 @@ class V8Plugin {
*/ */
val afterBusy = Event1<Int>(); val afterBusy = Event1<Int>();
val onScriptException = Event1<ScriptException>();
constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) { constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) {
this._client = client; this._client = client;
this._clientAuth = clientAuth; this._clientAuth = clientAuth;
@@ -217,7 +219,13 @@ class V8Plugin {
} }
fun <T : Any> catchScriptErrors(context: String, code: String? = null, handle: ()->T): T { fun <T : Any> catchScriptErrors(context: String, code: String? = null, handle: ()->T): T {
return catchScriptErrors(this.config, context, code, handle); try {
return catchScriptErrors(this.config, context, code, handle);
}
catch(ex: ScriptException) {
onScriptException.emit(ex);
throw ex;
}
} }
companion object { companion object {
@@ -242,7 +250,7 @@ class V8Plugin {
if(result is V8ValueObject) { if(result is V8ValueObject) {
val type = result.getString("plugin_type"); val type = result.getString("plugin_type");
if(type != null && type.endsWith("Exception")) if(type != null && type.endsWith("Exception"))
Companion.throwExceptionFromV8( throwExceptionFromV8(
config, config,
result.getOrThrow(config, "plugin_type", "V8Plugin"), result.getOrThrow(config, "plugin_type", "V8Plugin"),
result.getOrThrow(config, "message", "V8Plugin"), result.getOrThrow(config, "message", "V8Plugin"),
@@ -261,26 +269,26 @@ class V8Plugin {
catch(executeEx: JavetExecutionException) { catch(executeEx: JavetExecutionException) {
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) { if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) {
val pluginType = executeEx.scriptingError.context["plugin_type"].toString(); val pluginType = executeEx.scriptingError.context["plugin_type"].toString();
//Captcha
if (pluginType == "CaptchaRequiredException") { if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config, throw ScriptCaptchaRequiredException(config,
executeEx.scriptingError.context["url"].toString(), executeEx.scriptingError.context["url"]?.toString(),
executeEx.scriptingError.context["body"].toString(), executeEx.scriptingError.context["body"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped) executeEx, executeEx.scriptingError?.stack, codeStripped);
}; }
val exMessage = extractJSExceptionMessage(executeEx); //Others
throwExceptionFromV8( throwExceptionFromV8(
config, config,
pluginType, pluginType,
(exMessage ?: ""), (extractJSExceptionMessage(executeEx) ?: ""),
executeEx, executeEx,
executeEx.scriptingError?.stack, executeEx.scriptingError?.stack,
codeStripped codeStripped
); );
} }
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
val exMessage = extractJSExceptionMessage(executeEx);
throw ScriptExecutionException(config, "${exMessage}", null, executeEx.scriptingError?.stack, codeStripped);
} }
catch(ex: Exception) { catch(ex: Exception) {
throw ex; throw ex;
@@ -2,15 +2,17 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow 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) { 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 { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
val contextName = "ScriptCaptchaRequiredException";
return ScriptCaptchaRequiredException(config, return ScriptCaptchaRequiredException(config,
obj.getOrThrow(config, "url", "ScriptCaptchaRequiredException"), obj.getOrDefault<String>(config, "url", contextName, null),
obj.getOrThrow(config, "body", "ScriptCaptchaRequiredException")); obj.getOrDefault<String>(config, "body", contextName, null));
} }
} }
} }
@@ -98,20 +98,6 @@ class HomeFragment : MainFragment() {
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope) StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
}) })
.success { loadedResult(it); } .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> { .exception<ScriptExecutionException> {
Logger.w(ChannelFragment.TAG, "Plugin failure.", it); 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, UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0,
@@ -1,15 +1,51 @@
package com.futo.platformplayer.others package com.futo.platformplayer.others
import android.webkit.* import android.webkit.*
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginCaptchaConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import kotlinx.serialization.encodeToString
class CaptchaWebViewClient : WebViewClient { class CaptchaWebViewClient : WebViewClient {
val onCaptchaFinished = Event1<String>(); val onCaptchaFinished = Event1<SourceCaptchaData?>();
val onPageLoaded = Event2<WebView?, String?>() val onPageLoaded = Event2<WebView?, String?>()
constructor() : super() {} private val _pluginConfig: SourcePluginConfig?;
private val _captchaConfig: SourcePluginCaptchaConfig;
private val _extractor: WebViewRequirementExtractor;
constructor(config: SourcePluginConfig) : super() {
_pluginConfig = config;
_captchaConfig = config.captcha!!;
_extractor = WebViewRequirementExtractor(
config.allowUrls,
null,
null,
config.captcha!!.cookiesToFind,
config.captcha!!.completionUrl,
config.captcha!!.cookiesExclOthers
);
Logger.i(TAG, "Captcha [${config.name}]" +
"\nRequired Cookies: ${Serializer.json.encodeToString(config.captcha!!.cookiesToFind)}",);
}
constructor(captcha: SourcePluginCaptchaConfig) : super() {
_pluginConfig = null;
_captchaConfig = captcha;
_extractor = WebViewRequirementExtractor(
null,
null,
null,
captcha.cookiesToFind,
captcha.completionUrl,
captcha.cookiesExclOthers
);
}
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url); super.onPageFinished(view, url);
@@ -21,12 +57,12 @@ class CaptchaWebViewClient : WebViewClient {
if(request == null) if(request == null)
return super.shouldInterceptRequest(view, request as WebResourceRequest?); return super.shouldInterceptRequest(view, request as WebResourceRequest?);
Logger.i(TAG, "shouldInterceptRequest url = ${request.url}") val extracted = _extractor.handleRequest(view, request);
if (request.url.isHierarchical) { if(extracted != null) {
val googleAbuse = request.url.getQueryParameter("google_abuse"); onCaptchaFinished.emit(SourceCaptchaData(
if (googleAbuse != null) { extracted.cookies,
onCaptchaFinished.emit(googleAbuse); extracted.headers
} ));
} }
return super.shouldInterceptRequest(view, request); return super.shouldInterceptRequest(view, request);
@@ -46,6 +46,7 @@ class LoginWebViewClient : WebViewClient {
onPageLoaded.emit(view, url); onPageLoaded.emit(view, url);
} }
//TODO: Use new WebViewRequirementExtractor when time to test extensively
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
if(request == null) if(request == null)
return super.shouldInterceptRequest(view, request as WebResourceRequest?); return super.shouldInterceptRequest(view, request as WebResourceRequest?);
@@ -0,0 +1,125 @@
package com.futo.platformplayer.others
import android.net.Uri
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
class WebViewRequirementExtractor {
private val allowedUrls: List<String>;
private val headersToFind: List<String>?;
private val domainHeadersToFind: Map<String, List<String>>?;
private val cookiesToFind: List<String>?;
private val completionUrl: String?;
private val exclOtherCookies: Boolean;
private val headersFoundMap: HashMap<String, HashMap<String, String>> = hashMapOf();
private val cookiesFoundMap = hashMapOf<String, HashMap<String, String>>();
private var urlFound = false;
constructor(allowedUrls: List<String>?, headers: List<String>?, domainHeaders: Map<String, List<String>>?, cookies: List<String>?, url: String?, exclOtherCookies: Boolean = false) {
this.allowedUrls = allowedUrls ?: listOf("everywhere");
this.exclOtherCookies = exclOtherCookies;
headersToFind = headers;
domainHeadersToFind = domainHeaders;
cookiesToFind = cookies;
completionUrl = url;
}
fun handleRequest(view: WebView?, request: WebResourceRequest, logVerbose: Boolean = false): ExtractedData? {
val domain = request.url.host;
val domainLower = request.url.host?.lowercase();
if(completionUrl == null)
urlFound = true;
else urlFound = urlFound || request.url == Uri.parse(completionUrl);
//HEADERS
if(domainLower != null) {
val headersToFind = ((headersToFind?.map { Pair(it.lowercase(), domainLower) } ?: listOf()) +
(domainHeadersToFind?.filter { domainLower.matchesDomain(it.key.lowercase())}
?.flatMap { it.value.map { header -> Pair(header.lowercase(), it.key.lowercase()) } } ?: listOf()));
val foundHeaders = request.requestHeaders.filter { requestHeader -> headersToFind.any { it.first.equals(requestHeader.key, true)} &&
(!requestHeader.key.equals("Authorization", ignoreCase = true) || requestHeader.value != "undefined") } //TODO: More universal fix (optional regex?)
for(header in foundHeaders) {
for(headerDomain in headersToFind.filter { it.first.equals(header.key, true) }) {
if (!headersFoundMap.containsKey(headerDomain.second))
headersFoundMap[headerDomain.second] = hashMapOf();
headersFoundMap[headerDomain.second]!![header.key.lowercase()] = header.value;
}
}
}
//COOKIES
//TODO: This is not an ideal solution, we want to intercept the response, but interception need to be rewritten to support that. Correct implementation commented underneath
//TODO: For now we assume cookies are legit for all subdomains of a top-level domain, this is the most common scenario anyway
val cookieString = CookieManager.getInstance().getCookie(request.url.toString());
if(cookieString != null) {
val domainParts = domain!!.split(".");
val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
if(allowedUrls.any { it == "everywhere" || it.lowercase().matchesDomain(cookieDomain) })
cookiesToFind?.let { cookiesToFind ->
val cookies = cookieString.split(";");
for(cookieStr in cookies) {
val cookieSplitIndex = cookieStr.indexOf("=");
if(cookieSplitIndex <= 0) continue;
val cookieKey = cookieStr.substring(0, cookieSplitIndex).trim();
val cookieVal = cookieStr.substring(cookieSplitIndex + 1).trim();
if (exclOtherCookies && !cookiesToFind.contains(cookieKey))
continue;
if (cookiesFoundMap.containsKey(cookieDomain))
cookiesFoundMap[cookieDomain]!![cookieKey] = cookieVal;
else
cookiesFoundMap[cookieDomain] = hashMapOf(Pair(cookieKey, cookieVal));
}
};
}
val headersFound = headersToFind?.map { it.lowercase() }?.all { reqHeader -> headersFoundMap.any { it.value.containsKey(reqHeader) } } ?: true
val domainHeadersFound = domainHeadersToFind?.all {
if(it.value.isEmpty())
return@all true;
if(!headersFoundMap.containsKey(it.key.lowercase()))
return@all false;
val foundDomainHeaders = headersFoundMap[it.key.lowercase()] ?: mapOf();
return@all it.value.all { reqHeader -> foundDomainHeaders.containsKey(reqHeader.lowercase()) };
} ?: true;
val cookiesFound = cookiesToFind?.all { toFind -> cookiesFoundMap.any { it.value.containsKey(toFind) } } ?: true;
if(logVerbose) {
val builder = StringBuilder();
builder.appendLine("Request (method: ${request.method}, host: ${request.url.host}, url: ${request.url}, path: ${request.url.path}):");
for (pair in request.requestHeaders) {
builder.appendLine(" ${pair.key}: ${pair.value}");
}
builder.appendLine(" Cookies: ${cookiesFoundMap.values.sumOf { it.values.size }}");
Logger.i(TAG, builder.toString());
Logger.i(TAG, "Result (urlFound: $urlFound, headersFound: $headersFound, cookiesFound: $cookiesFound)");
}
if (urlFound && headersFound && domainHeadersFound && cookiesFound)
return ExtractedData(cookiesFoundMap, headersFoundMap);
return null;
}
data class ExtractedData(
val cookies: HashMap<String, HashMap<String, String>>,
val headers: HashMap<String, HashMap<String, String>>
);
companion object {
val TAG = "WebViewRequirementExtractor";
}
}
@@ -25,11 +25,17 @@ import androidx.lifecycle.lifecycleScope
import androidx.work.* import androidx.work.*
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.activities.CaptchaActivity
import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.background.BackgroundWorker import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
import com.futo.platformplayer.logging.AndroidLogConsumer import com.futo.platformplayer.logging.AndroidLogConsumer
import com.futo.platformplayer.logging.FileLogConsumer import com.futo.platformplayer.logging.FileLogConsumer
import com.futo.platformplayer.logging.LogLevel import com.futo.platformplayer.logging.LogLevel
@@ -637,6 +643,26 @@ class StateApp {
} }
} }
fun handleCaptchaException(client: JSClient, exception: ScriptCaptchaRequiredException) {
Logger.w(HomeFragment.TAG, "[${client.name}] Plugin captcha required.", exception);
scopeOrNull?.launch(Dispatchers.Main) {
UIDialogs.showConfirmationDialog(context, "Captcha required\nPlugin [${client.config.name}]", {
CaptchaActivity.showCaptcha(context, client.config, exception.url, exception.body) {
StatePlugins.instance.setPluginCaptcha(client.config.id, it);
scopeOrNull?.launch(Dispatchers.IO) {
try {
StatePlatform.instance.reloadClient(context, client.config.id);
} catch (e: Throwable) {
Logger.e(SourceDetailFragment.TAG, "Failed to reload client.", e)
return@launch;
}
}
}
})
}
}
companion object { companion object {
private val TAG = "StateApp"; private val TAG = "StateApp";
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive @SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
@@ -172,7 +172,11 @@ class StatePlatform {
_icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?: _icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
ImageVariable(plugin.config.absoluteIconUrl, null); ImageVariable(plugin.config.absoluteIconUrl, null);
_availableClients.add(JSClient(context, plugin)); val client = JSClient(context, plugin);
client.onCaptchaException.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex);
}
_availableClients.add(client);
} }
if(_availableClients.distinctBy { it.id }.count() < _availableClients.size) if(_availableClients.distinctBy { it.id }.count() < _availableClients.size)
@@ -287,6 +291,9 @@ class StatePlatform {
StatePlugins.instance.getPlugin(id) StatePlugins.instance.getPlugin(id)
?: throw IllegalStateException("Client existed, but plugin config didn't") ?: throw IllegalStateException("Client existed, but plugin config didn't")
); );
newClient.onCaptchaException.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex);
}
synchronized(_clientsLock) { synchronized(_clientsLock) {
if (_enabledClients.contains(client)) { if (_enabledClients.contains(client)) {
@@ -6,6 +6,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@@ -372,7 +373,7 @@ class StatePlugins {
if(icon != null) if(icon != null)
iconsDir.saveIconBinary(config.id, icon); iconsDir.saveIconBinary(config.id, icon);
_plugins.save(SourcePluginDescriptor(config, null, flags)); _plugins.save(SourcePluginDescriptor(config, null, null, flags));
return null; return null;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
@@ -407,6 +408,18 @@ class StatePlugins {
} }
} }
fun setPluginCaptcha(id: String, captcha: SourceCaptchaData?) {
if(id == StateDeveloper.DEV_ID) {
StatePlatform.instance.getDevClient()?.let {
it.setCaptcha(captcha);
};
return;
}
val descriptor = getPlugin(id) ?: throw IllegalArgumentException("Plugin [${id}] does not exist");
descriptor.updateCaptcha(captcha);
_plugins.save(descriptor);
}
fun setPluginAuth(id: String, auth: SourceAuth?) { fun setPluginAuth(id: String, auth: SourceAuth?) {
if(id == StateDeveloper.DEV_ID) { if(id == StateDeveloper.DEV_ID) {
StatePlatform.instance.getDevClient()?.let { StatePlatform.instance.getDevClient()?.let {