mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Captcha plugin system
This commit is contained in:
@@ -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())
|
||||||
|
}
|
||||||
+12
@@ -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(),
|
||||||
|
|||||||
+16
-3
@@ -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();
|
||||||
|
|||||||
+33
-35
@@ -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;
|
||||||
|
|||||||
+5
-3
@@ -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 {
|
||||||
|
|||||||
Submodule app/src/unstable/assets/sources/youtube updated: 40c9307c06...1c34bb0163
Reference in New Issue
Block a user