Skip to content

Commit

Permalink
Handle HTTP redirect by hooking httpEngine
Browse files Browse the repository at this point in the history
This should be faster than sending two requests from JavaScript
  • Loading branch information
JingMatrix committed Sep 24, 2023
1 parent 35509fe commit 9054618
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 65 deletions.
81 changes: 31 additions & 50 deletions app/src/main/assets/GM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions app/src/main/java/org/matrix/chromext/Chrome.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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<String?, List<String>>
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<String?, List<String>>,
) {
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) {
Expand Down
25 changes: 10 additions & 15 deletions app/src/main/java/org/matrix/chromext/utils/XMLHttpRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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,
Expand Down

0 comments on commit 9054618

Please sign in to comment.