From 90546186fc64dbcec669750459af91d7c7771217 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 24 Sep 2023 21:39:13 +0200 Subject: [PATCH] Handle HTTP redirect by hooking httpEngine This should be faster than sending two requests from JavaScript --- app/src/main/assets/GM.js | 81 +++++++------------ .../main/java/org/matrix/chromext/Chrome.kt | 46 +++++++++++ .../matrix/chromext/utils/XMLHttpRequest.kt | 25 +++--- 3 files changed, 87 insertions(+), 65 deletions(-) diff --git a/app/src/main/assets/GM.js b/app/src/main/assets/GM.js index 82d65559..5a1fa6ad 100644 --- a/app/src/main/assets/GM.js +++ b/app/src/main/assets/GM.js @@ -601,16 +601,6 @@ function GM_xmlhttpRequest(details) { ChromeXt.removeEventListener("xmlhttpRequest", listener); } - function redirect(response) { - const request = { ...details, url: response.finalUrl }; - if ([301, 302, 303].includes(response.status)) { - request.method = "GET"; - delete request.data; - delete request.binary; - } - return request; - } - const xhrHandler = { target: new EventTarget(), get() { @@ -678,6 +668,10 @@ function GM_xmlhttpRequest(details) { sink.parse(data); if (type == "progress") { sink.writer.write(data); + } else if (type == "redirect" && xhr.redirect == "error") { + xhr.error = new Error("Redirection not allowed"); + sink.dispatch("error"); + xhr.abort("redirect"); } else if (type == "load") { sink.writer .close() @@ -724,17 +718,8 @@ function GM_xmlhttpRequest(details) { abort: true, }); revoke(listener); - if (type == "redirect") { - if (xhr.redirect == "error") { - xhr.error = new Error("Redirection not allowed"); - } else if (xhr.redirect == "follow") { - GM_xmlhttpRequest(redirect(xhr)) - .then((res) => resolve(res)) - .catch((e) => reject(e)); - } - } if (xhr.error instanceof Error) reject(xhr.error); - sink.writer.abort(type, type != "redirect"); + sink.writer.abort(type); }; let request = details; @@ -768,7 +753,6 @@ function GM_xmlhttpRequest(details) { xhr.responseType ); xhr.readyState = 1; - xhr.redirect = details.redirect || "follow"; if (useJSFetch) { request.signal.addEventListener("abort", xhr.abort); @@ -924,49 +908,46 @@ class ResponseSink { parse(data) { if (typeof data != "object") return; for (const prop in data) { - if (prop == "headers") continue; + if (prop == "headers" || prop == "status") continue; const val = data[prop]; if (typeof val == "function") continue; this.xhr[prop] = val; } - if (this.xhr.readyState != 1) return; - const headers = data.headers; - if (typeof headers != "object" || this.xhr.headers instanceof Headers) - return; - if (headers instanceof Headers) { - this.xhr.headers = headers; - } else { - Object.defineProperty(this.xhr, "headers", { value: new Headers() }); - Object.entries(headers).forEach(([k, vs]) => { - for (const v of vs) { - this.xhr.headers.append(k, v); - } + if (this.xhr.status == data.status) return; + // status change if there are redirections + + this.xhr.status = data.status; + + let headers = data.headers; + if (!(headers instanceof Headers)) { + const entries = Object.entries(headers); + headers = new Headers(); + entries.forEach(([k, vs]) => { + for (const v of vs) headers.append(k, v); }); } + this.xhr.headers = headers; this.xhr.readyState = 2; - this.xhr.responseHeaders = Object.entries( - Object.fromEntries(this.xhr.headers) - ) + const responseHeaders = Object.entries(Object.fromEntries(headers)) .map(([k, v]) => k.toLowerCase() + ": " + v) .join("\r\n"); - this.xhr.getAllResponseHeaders = () => this.xhr.responseHeaders; - this.xhr.getResponseHeader = (headerName) => - this.xhr.headers.get(headerName); - this.xhr.finalUrl = this.xhr.headers.get("Location") || this.xhr.url; + this.xhr.responseHeaders = responseHeaders; + this.xhr.getAllResponseHeaders = () => responseHeaders; + this.xhr.getResponseHeader = (headerName) => headers.get(headerName); + this.xhr.finalUrl = headers.get("Location") || this.xhr.url; this.xhr.responseURL = this.xhr.finalUrl; - if (this.xhr.finalUrl != this.xhr.url) { - this.close().then(() => this.xhr.abort("redirect")); - } - this.xhr.total = this.xhr.headers.get("Content-Length"); + this.xhr.total = headers.get("Content-Length"); if (this.xhr.total !== null) { this.xhr.lengthComputable = true; this.xhr.total = Number(this.xhr.total); } + if (this.xhr.finalUrl != this.xhr.url && this.xhr.redirect != "error") + this.dispatch("redirect", { ...this.xhr }); if (data instanceof Response) return; - this.xhr.encoding = this.xhr.headers.get("Content-Encoding"); - if (this.xhr.encoding != null) { + const encoding = headers.get("Content-Encoding"); + if (encoding != null) { try { - this.ds = new DecompressionStream(this.xhr.encoding.toLowerCase()); + this.ds = new DecompressionStream(encoding.toLowerCase()); } catch { this.xhr.abort(); } @@ -1026,9 +1007,9 @@ class ResponseSink { this.dispatch("loadend"); if (parseError instanceof Error) throw parseError; } - abort(reason, loadend = true) { + abort(reason) { this.dispatch(reason); - if (loadend) this.dispatch("loadend"); + this.dispatch("loadend"); } } // Kotlin separator diff --git a/app/src/main/java/org/matrix/chromext/Chrome.kt b/app/src/main/java/org/matrix/chromext/Chrome.kt index e123321e..a5ad282f 100644 --- a/app/src/main/java/org/matrix/chromext/Chrome.kt +++ b/app/src/main/java/org/matrix/chromext/Chrome.kt @@ -10,7 +10,9 @@ import android.os.Handler import java.io.File import java.lang.ref.WeakReference import java.net.CookieManager +import java.net.HttpCookie import java.util.concurrent.Executors +import org.json.JSONArray import org.json.JSONObject import org.matrix.chromext.devtools.DevSessions import org.matrix.chromext.devtools.getInspectPages @@ -20,7 +22,10 @@ import org.matrix.chromext.hook.WebViewHook import org.matrix.chromext.proxy.UserScriptProxy import org.matrix.chromext.script.Local import org.matrix.chromext.utils.Log +import org.matrix.chromext.utils.XMLHttpRequest import org.matrix.chromext.utils.findField +import org.matrix.chromext.utils.findMethod +import org.matrix.chromext.utils.hookAfter import org.matrix.chromext.utils.invokeMethod object Chrome { @@ -50,7 +55,9 @@ object Chrome { isVivaldi = packageName == "com.vivaldi.browser" @Suppress("DEPRECATION") val packageInfo = ctx.packageManager?.getPackageInfo(packageName, 0) Log.i("Package: ${packageName}, v${packageInfo?.versionName}") + setupHttpCache(ctx) + saveRedirectCookie() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val groupId = "org.matrix.chromext" val group = NotificationChannelGroup(groupId, "ChromeXt") @@ -82,6 +89,45 @@ object Chrome { HttpResponseCache.install(httpCacheDir, httpCacheSize) } + private fun saveRedirectCookie() { + val httpEngine = load("com.android.okhttp.internal.http.HttpEngine") + val userRequest = findField(httpEngine) { name == "userRequest" } + val userResponse = findField(httpEngine) { name == "userResponse" } + val urlString = findMethod(userRequest.type) { name == "urlString" } + val headers = findField(userResponse.type) { name == "headers" } + val code = findField(userResponse.type) { name == "code" } + val message = findField(userResponse.type) { name == "message" } + val toMultimap = findMethod(headers.type) { name == "toMultimap" } + findMethod(httpEngine) { name == "followUpRequest" } + .hookAfter { + if (it.result != null) { + val url = urlString.invoke(userRequest.get(it.thisObject)) as String + val request = Listener.xmlhttpRequests.values.find { it.url.toString() == url } + if (request == null || request.anonymous) return@hookAfter + val res = userResponse.get(it.thisObject) + @Suppress("UNCHECKED_CAST") + val headerFields = toMultimap.invoke(headers.get(res)) as Map> + storeCoookies(request, headerFields) + val data = JSONObject() + data.put("status", code.get(res) as Int) + data.put("statusText", message.get(res) as String) + data.put("headers", JSONObject(headerFields.mapValues { JSONArray(it.value) })) + request.response("redirect", data, false) + } + } + } + + fun storeCoookies( + request: XMLHttpRequest, + headerFields: Map>, + ) { + headerFields + .filter { it.key != null && it.key!!.lowercase().startsWith("set-cookie") } + .forEach { + it.value.forEach { HttpCookie.parse(it).forEach { cookieStore.add(request.uri, it) } } + } + } + fun wakeUpDevTools(limit: Int = 10) { var waited = 0 while (!devToolsReady && waited < limit) { diff --git a/app/src/main/java/org/matrix/chromext/utils/XMLHttpRequest.kt b/app/src/main/java/org/matrix/chromext/utils/XMLHttpRequest.kt index a11f7422..12c84536 100644 --- a/app/src/main/java/org/matrix/chromext/utils/XMLHttpRequest.kt +++ b/app/src/main/java/org/matrix/chromext/utils/XMLHttpRequest.kt @@ -51,7 +51,7 @@ class XMLHttpRequest(id: String, request: JSONObject, uuid: Double, currentTab: connection = url.openConnection() as HttpURLConnection with(connection!!) { setRequestMethod(method) - setInstanceFollowRedirects(false) + setInstanceFollowRedirects(request.optString("redirect") != "manual") headers?.keys()?.forEach { setRequestProperty(it, headers.optString(it)) } setUseCaches(!nocache) setConnectTimeout(timeout) @@ -86,20 +86,12 @@ class XMLHttpRequest(id: String, request: JSONObject, uuid: Double, currentTab: data.put("headers", JSONObject(headers)) val binary = responseType !in listOf("", "text", "document", "json") || - contentEncoding != "" || - contentType.contains("charset") + contentEncoding != null || + (contentType != null && + contentType.contains("charset") && + !contentType.contains("utf-8")) data.put("binary", binary) - if (!anonymous) { - headerFields - .filter { it.key != null && it.key.lowercase().startsWith("set-cookie") } - .forEach { - it.value.forEach { - HttpCookie.parse(it).forEach { Chrome.cookieStore.add(uri, it) } - } - } - } - val buffer = ByteArray(buffersize * DEFAULT_BUFFER_SIZE) while (true) { var bytes = 0 @@ -120,7 +112,7 @@ class XMLHttpRequest(id: String, request: JSONObject, uuid: Double, currentTab: response("progress", data, false) data.remove("headers") } - response("load", data, false) + response("load", data) } .onFailure { if (it is IOException) { @@ -136,9 +128,12 @@ class XMLHttpRequest(id: String, request: JSONObject, uuid: Double, currentTab: } } } + if (!anonymous && connection != null) { + Chrome.storeCoookies(this, connection!!.headerFields) + } } - private fun response( + fun response( type: String, data: JSONObject = JSONObject(), disconnect: Boolean = true,