From 819e81b7a6006b8860668d1c7f8b0be545c6094f Mon Sep 17 00:00:00 2001 From: Kelvin Date: Mon, 20 May 2024 22:28:51 +0200 Subject: [PATCH] Proxy support, Additional http header access support --- app/src/main/assets/devportal/dev_bridge.js | 11 ++ app/src/main/assets/devportal/index.html | 147 +++++++++++++++++- .../media/platforms/js/SourcePluginConfig.kt | 8 +- .../platforms/js/internal/JSHttpClient.kt | 28 +++- .../developer/DeveloperEndpoints.kt | 34 +++- .../engine/packages/PackageHttp.kt | 36 +++-- .../platformplayer/states/StateDeveloper.kt | 26 ++++ app/src/stable/assets/sources/bilibili | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/bilibili | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 11 files changed, 275 insertions(+), 23 deletions(-) diff --git a/app/src/main/assets/devportal/dev_bridge.js b/app/src/main/assets/devportal/dev_bridge.js index b89f303d..34271a18 100644 --- a/app/src/main/assets/devportal/dev_bridge.js +++ b/app/src/main/assets/devportal/dev_bridge.js @@ -262,6 +262,17 @@ function getDevLogs(lastIndex, cb) { .then(x=>x.json()) .then(y=> cb && cb(y)); } +function getDevHttpExchanges(cb) { + fetch("/plugin/getDevHttpExchanges", { + timeout: 1000 + }) + .then(x=>x.json()) + .then(y=> cb && cb(y)); +} +function setDevHttpProxy(url, port) { + return fetch("/dev/setDevProxy?url=" + encodeURIComponent(url) + "&port=" + port) + .then(x=>x.json()); +} function sendFakeDevLog(devId, msg) { return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {}); } diff --git a/app/src/main/assets/devportal/index.html b/app/src/main/assets/devportal/index.html index a2134eef..9ae84f5a 100644 --- a/app/src/main/assets/devportal/index.html +++ b/app/src/main/assets/devportal/index.html @@ -196,6 +196,79 @@ padding-top: 50px; font-family: sans-serif; } + .httpContainer { + position: relative; + } + .httpLine { + } + .httpLine .request { + height: 50px; + position: relative; + cursor: pointer; + } + .httpLine .request .status { + position: absolute; + left: 10px; + width: 40px; + top: 10px; + padding: 5px; + background-color: #333; + border-radius: 5px; + text-align: center; + } + .httpLine .request .status.error { + background-color: #880000; + } + .httpLine .request .status.success { + background-color: #008800; + } + .httpLine .request .status.warn { + background-color: #803500; + } + .httpLine .request .method { + position: absolute; + left: 55px; + top: 10px; + padding: 5px; + background-color: #333; + border-radius: 5px; + width: 50px; + text-align: center; + } + .httpLine .request .url { + position: absolute; + left: 110px; + top: 10px; + padding: 5px; + background-color: #333; + border-radius: 5px; + } + .httpLine .response { + background-color: #111; + margin-left: 55px; + border-radius: 6px; + padding: 10px; + } + .httpLine .response .body{ + white-space: pre-wrap; + font-family: monospace; + background-color: black; + padding: 10px; + } + .httpLine .response .headers { + margin: 10px; + } + .httpLine .response .headers .key { + display: inline-block; + font-weight: bold; + font-size: 14px; + color: #FFF; + } + .httpLine .response .headers .value { + display: inline-block; + font-size: 14px; + color: #AAA; + } @@ -547,7 +620,62 @@ - Clear + Clear + + + + + Http Logs + + + +
+ +
+
+
+
+
+ {{exchange.response.status}} +
+
+ {{exchange.request.method}} +
+
+ {{exchange.request.url}} +
+
+
+

Request Headers

+
+
+
+ {{header}} +
+
+ {{headerValue}} +
+
+
+

Response

+
+
+
+ {{header}} +
+
+ {{headerValue}} +
+
+
+
{{exchange.response.body}}
+
+
+
+
+ + + Clear
@@ -604,7 +732,9 @@ lastLogIndex: -1, lastLogDevID: "", logs: [], - lastInjectTime: "" + httpExchanges: [], + lastInjectTime: "", + showHttpRequests: false }, Plugin: { loadUsingTag: false, @@ -688,6 +818,16 @@ }); } }); + if(this.Integration.showHttpRequests) { + getDevHttpExchanges((exchanges)=>{ + Vue.nextTick(()=>{ + for(i = 0; i < exchanges.length; i++) { + exchanges[i].response.show = false; + this.Integration.httpExchanges.unshift(exchanges[i]); + } + }); + }); + } } catch(ex) { console.error("Failed update", ex); @@ -970,6 +1110,9 @@ }, showTestResults(results) { + }, + toggleHttpExchange(exchange) { + exchange.response.show = !exchange.response.show; }, copyClipboard(cpy) { if(navigator.clipboard) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt index e279fc0f..21193f13 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt @@ -46,7 +46,8 @@ class SourcePluginConfig( var enableInHome: Boolean = true, var supportedClaimTypes: List = listOf(), var primaryClaimFieldType: Int? = null, - var developerSubmitUrl: String? = null + var developerSubmitUrl: String? = null, + var allowAllHttpHeaderAccess: Boolean = false, ) : IV8PluginConfig { val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl); @@ -143,6 +144,11 @@ class SourcePluginConfig( list.add(Pair( "Unrestricted Web Access", "This plugin requires access to all URLs, this may include malicious URLs.")); + if(allowAllHttpHeaderAccess) + list.add(Pair( + "Unrestricted Http Header access", + "Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests." + )) return list; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt index 8496dfc7..0e9b29ab 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt @@ -2,14 +2,22 @@ package com.futo.platformplayer.api.media.platforms.js.internal import android.net.Uri import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.api.media.platforms.js.DevJSClient 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.SourceCaptchaData import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.models.JSRequest import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier +import com.futo.platformplayer.developer.DeveloperEndpoints import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.matchesDomain +import com.futo.platformplayer.states.StateDeveloper +import com.google.common.net.MediaType +import okhttp3.OkHttpClient +import okio.GzipSource +import java.net.InetSocketAddress +import java.net.Proxy import java.util.UUID class JSHttpClient : ManagedHttpClient { @@ -28,7 +36,15 @@ class JSHttpClient : ManagedHttpClient { private var _currentCookieMap: HashMap>; private var _otherCookieMap: HashMap>; - constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() { + constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super( + //Temporary ugly solution for DevPortal proxy support + (if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null) + OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP, + InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port) + )) + else + OkHttpClient.Builder()) + ) { _jsClient = jsClient; _jsConfig = config; _auth = auth; @@ -201,6 +217,16 @@ class JSHttpClient : ManagedHttpClient { } } } + + if(_jsClient is DevJSClient) { + //val peekBody = resp.peekBody(1000 * 1000).string(); + StateDeveloper.instance.addDevHttpExchange( + StateDeveloper.DevHttpExchange( + StateDeveloper.DevHttpRequest(resp.request.method, resp.request.url.toString(), mapOf(*resp.request.headers.map { Pair(it.first, it.second) }.toTypedArray()), ""), + StateDeveloper.DevHttpRequest("RESP", resp.request.url.toString(), mapOf(*resp.headers.map { Pair(it.first, it.second) }.toTypedArray()), "", resp.code) + )); + } + return resp; } diff --git a/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt b/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt index 793f2073..e386681c 100644 --- a/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt +++ b/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt @@ -116,12 +116,6 @@ class DeveloperEndpoints(private val context: Context) { } //Dependencies - //@HttpGET("/dependencies/vue.js", "application/javascript") - //val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true); - //@HttpGET("/dependencies/vuetify.js", "application/javascript") - //val depVuetify = StateAssets.readAsset(context, "devportal/dependencies/vuetify.js", true); - //@HttpGET("/dependencies/vuetify.min.css", "text/css") - //val depVuetifyCss = StateAssets.readAsset(context, "devportal/dependencies/vuetify.min.css", true); @HttpGET("/dependencies/FutoMainLogo.svg", "image/svg+xml") val depFutoLogo = StateAssets.readAsset(context, "devportal/dependencies/FutoMainLogo.svg"); @HttpGET("/favicon.svg", "image/svg+xml") @@ -450,6 +444,25 @@ class DeveloperEndpoints(private val context: Context) { context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain") } } + @HttpGET("/dev/setDevProxy") + fun devSetDevProxy(context: HttpContext) { + try { + val url = context.query.getOrDefault("url", ""); + val port = context.query.getOrDefault("port", ""); + if(url.isNullOrEmpty() || port.isNullOrEmpty() || port.toIntOrNull() == null) + { + StateDeveloper.instance.devProxy = null; + context.respondCode(400); + return; + } + StateDeveloper.instance.devProxy = StateDeveloper.DevProxySettings(url, port.toInt()); + context.respondCode(200, "true", "application/json"); + } + catch(ex: Exception) { + Logger.e("DeveloperEndpoints", ex.message, ex); + context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain") + } + } @HttpGET("/plugin/getDevLogs") fun pluginGetDevLogs(context: HttpContext) { @@ -461,6 +474,15 @@ class DeveloperEndpoints(private val context: Context) { context.respondCode(500, ex.message ?: "", "text/plain") } } + @HttpGET("/plugin/getDevHttpExchanges") + fun pluginGetDevExchanges(context: HttpContext) { + try { + context.respondJson(200, StateDeveloper.instance.getHttpExchangesAndClear()); + } + catch(ex: Exception) { + context.respondCode(500, ex.message ?: "", "text/plain") + } + } @HttpGET("/plugin/fakeDevLog") fun pluginFakeDevLog(context: HttpContext) { try { diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt index b1b7f460..666bd2f5 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt @@ -9,6 +9,7 @@ import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8Plugin @@ -242,7 +243,8 @@ class PackageHttp: V8Package { val resp = client.requestMethod(method, url, headers); val responseBody = resp.body?.string(); //logResponse(method, url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers, + _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); } }; } @@ -256,7 +258,8 @@ class PackageHttp: V8Package { val resp = client.requestMethod(method, url, body, headers); val responseBody = resp.body?.string(); //logResponse(method, url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers, + _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); } }; } @@ -271,7 +274,8 @@ class PackageHttp: V8Package { val resp = client.get(url, headers); val responseBody = resp.body?.string(); //logResponse("GET", url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers, + _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); } }; } @@ -285,7 +289,8 @@ class PackageHttp: V8Package { val resp = client.post(url, body, headers); val responseBody = resp.body?.string(); //logResponse("POST", url, resp.code, resp.headers, responseBody); - return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers)); + return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers, + _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); } }; } @@ -305,12 +310,25 @@ class PackageHttp: V8Package { } } - private fun sanitizeResponseHeaders(headers: Map>?): Map> { + private fun sanitizeResponseHeaders(headers: Map>?, onlyWhitelisted: Boolean = false): Map> { val result = mutableMapOf>() - headers?.forEach { (header, values) -> - val lowerCaseHeader = header.lowercase() - if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) { - result[lowerCaseHeader] = values + if(onlyWhitelisted) + headers?.forEach { (header, values) -> + val lowerCaseHeader = header.lowercase() + if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) { + result[lowerCaseHeader] = values + } + } + else { + headers?.forEach { (header, values) -> + val lowerCaseHeader = header.lowercase() + if(lowerCaseHeader == "set-cookie") { + result[lowerCaseHeader] = values.filter{ + !it.lowercase().contains("httponly") + }; + } + else + result[lowerCaseHeader] = values; } } return result diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt index 7b6628e6..12904470 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt @@ -19,6 +19,9 @@ class StateDeveloper { private var _devLogsIndex: Int = 0; private val _devLogs: MutableList = mutableListOf(); + private val _devHttpExchanges: MutableList = mutableListOf(); + + var devProxy: DevProxySettings? = null; fun initializeDev(id: String) { currentDevID = id; @@ -94,6 +97,21 @@ class StateDeveloper { } } + fun addDevHttpExchange(exchange: DevHttpExchange) { + synchronized(_devHttpExchanges) { + if(_devHttpExchanges.size > 15) + _devHttpExchanges.removeAt(0); + _devHttpExchanges.add(exchange); + } + } + fun getHttpExchangesAndClear(): List { + synchronized(_devHttpExchanges) { + val data = _devHttpExchanges.toList(); + _devHttpExchanges.clear(); + return data; + } + } + fun setDevClientSettings(settings: HashMap) { val client = StatePlatform.instance.getDevClient(); client?.let { @@ -138,4 +156,12 @@ class StateDeveloper { @kotlinx.serialization.Serializable data class DevLog(val id: Int, val devId: String, val type: String, val log: String); + + @kotlinx.serialization.Serializable + data class DevHttpRequest(val method: String, val url: String, val headers: Map, val body: String, val status: Int = 0); + @kotlinx.serialization.Serializable + data class DevHttpExchange(val request: DevHttpRequest, val response: DevHttpRequest); + + @kotlinx.serialization.Serializable + data class DevProxySettings(val url: String, val port: Int) } \ No newline at end of file diff --git a/app/src/stable/assets/sources/bilibili b/app/src/stable/assets/sources/bilibili index 3cc6d553..59d2200f 160000 --- a/app/src/stable/assets/sources/bilibili +++ b/app/src/stable/assets/sources/bilibili @@ -1 +1 @@ -Subproject commit 3cc6d553cf840141fb5fa718a7b4a6b49282eaad +Subproject commit 59d2200f9220f2add3c4b7eccc314306503493a3 diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index cac27408..37e2ed94 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit cac27408440f5586c1c68d846456792041403d35 +Subproject commit 37e2ed94384ff82f4cb67a2250877cb1e8e03c57 diff --git a/app/src/unstable/assets/sources/bilibili b/app/src/unstable/assets/sources/bilibili index 3cc6d553..59d2200f 160000 --- a/app/src/unstable/assets/sources/bilibili +++ b/app/src/unstable/assets/sources/bilibili @@ -1 +1 @@ -Subproject commit 3cc6d553cf840141fb5fa718a7b4a6b49282eaad +Subproject commit 59d2200f9220f2add3c4b7eccc314306503493a3 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index d1058f0b..37e2ed94 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit d1058f0b6ccf8cbebe4eed2afba145899e6dba00 +Subproject commit 37e2ed94384ff82f4cb67a2250877cb1e8e03c57