From be63e88c770d148d9a923edb230d606a333d2d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Mon, 26 Jan 2026 09:13:35 +0100 Subject: [PATCH 1/6] catch failing JSONObject creation It turned out that the JS functions hand over "undefined", which leads to the JSONObject creation failing with an exception. Now we catch that (or any other incompatible string) and simply create an empty object. --- .../RequestInspectorJavaScriptInterface.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt index bf09d84..bb53f79 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -5,6 +5,7 @@ import android.webkit.JavascriptInterface import android.webkit.WebView import org.intellij.lang.annotations.Language import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject import java.net.URLEncoder @@ -134,7 +135,13 @@ internal class RequestInspectorJavaScriptInterface(webView: WebView) { } private fun getHeadersAsMap(headersString: String): MutableMap { - val headersObject = JSONObject(headersString) + val headersObject = try { + JSONObject(headersString) + } catch (_: JSONException) { + // When during the creation of a JSONObject from the string a JSONException is thrown, we simply return an + // empty JSONObject. This happens e.g. when JS send "undefined" or an empty string as headers. + JSONObject() + } val map = HashMap() for (key in headersObject.keys()) { val lowercaseHeader = key.lowercase() From b0f96573db3ebf37611c65801e3e12463aaaa02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Tue, 13 Jan 2026 16:52:00 +0100 Subject: [PATCH 2/6] refactor into matchers This refactoring is a preparation to allow different matchers to connect the recordedRequests with the WebResourceRequest. It introduces an interface to allow different matchers to be easily exchanged and adds the already existing matching by URL as a first matcher. --- .../RequestInspectorJavaScriptInterface.kt | 25 ++++--------- .../RequestInspectorWebViewClient.kt | 11 +++--- .../matcher/RequestMatcher.kt | 11 ++++++ .../matcher/RequestUrlMatcher.kt | 35 +++++++++++++++++++ 4 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt index bb53f79..7bc3a80 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -2,33 +2,23 @@ package com.acsbendi.requestinspectorwebview import android.util.Log import android.webkit.JavascriptInterface +import android.webkit.WebResourceRequest import android.webkit.WebView +import com.acsbendi.requestinspectorwebview.matcher.RequestMatcher import org.intellij.lang.annotations.Language import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.net.URLEncoder -internal class RequestInspectorJavaScriptInterface(webView: WebView) { +class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: RequestMatcher) { init { webView.addJavascriptInterface(this, INTERFACE_NAME) } - private val recordedRequests = ArrayList() - - fun findRecordedRequestForUrl(url: String): RecordedRequest? { - return synchronized(recordedRequests) { - // use findLast instead of find to find the last added query matching a URL - - // they are included at the end of the list when written. - recordedRequests.findLast { recordedRequest -> - // Added search by exact URL to find the actual request body - url == recordedRequest.url - } ?: recordedRequests.findLast { recordedRequest -> - // Previously, there was only a search by contains, and because of this, sometimes the wrong request body was found - url.contains(recordedRequest.url) - } - } + fun createWebViewRequest(request: WebResourceRequest): WebViewRequest { + return matcher.createWebViewRequest(request) } data class RecordedRequest( @@ -129,9 +119,7 @@ internal class RequestInspectorJavaScriptInterface(webView: WebView) { } private fun addRecordedRequest(recordedRequest: RecordedRequest) { - synchronized(recordedRequests) { - recordedRequests.add(recordedRequest) - } + matcher.addRecordedRequest(recordedRequest) } private fun getHeadersAsMap(headersString: String): MutableMap { @@ -165,7 +153,6 @@ internal class RequestInspectorJavaScriptInterface(webView: WebView) { return map } - private fun getUrlEncodedFormBody(formParameterJsonArray: JSONArray): String { val resultStringBuilder = StringBuilder() repeat(formParameterJsonArray.length()) { i -> diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt index 3b895fe..ff3b11b 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt @@ -7,14 +7,16 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient +import com.acsbendi.requestinspectorwebview.matcher.RequestMatcher +import com.acsbendi.requestinspectorwebview.matcher.RequestUrlMatcher @SuppressLint("SetJavaScriptEnabled") open class RequestInspectorWebViewClient @JvmOverloads constructor( - webView: WebView, + webView: WebView, val matcher: RequestMatcher = RequestUrlMatcher(), private val options: RequestInspectorOptions = RequestInspectorOptions() ) : WebViewClient() { - private val interceptionJavascriptInterface = RequestInspectorJavaScriptInterface(webView) + private val interceptionJavascriptInterface = RequestInspectorJavaScriptInterface(webView, matcher) init { val webSettings = webView.settings @@ -26,10 +28,7 @@ open class RequestInspectorWebViewClient @JvmOverloads constructor( view: WebView, request: WebResourceRequest ): WebResourceResponse? { - val recordedRequest = interceptionJavascriptInterface.findRecordedRequestForUrl( - request.url.toString() - ) - val webViewRequest = WebViewRequest.create(request, recordedRequest) + val webViewRequest = interceptionJavascriptInterface.createWebViewRequest(request) return shouldInterceptRequest(view, webViewRequest) } diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt new file mode 100644 index 0000000..f2d2880 --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt @@ -0,0 +1,11 @@ +package com.acsbendi.requestinspectorwebview.matcher + +import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface.RecordedRequest +import android.webkit.WebResourceRequest +import com.acsbendi.requestinspectorwebview.WebViewRequest + +interface RequestMatcher { + fun addRecordedRequest(recordedRequest: RecordedRequest) + fun createWebViewRequest(request: WebResourceRequest): WebViewRequest +} + diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt new file mode 100644 index 0000000..f5eda74 --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt @@ -0,0 +1,35 @@ +package com.acsbendi.requestinspectorwebview.matcher + +import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface.RecordedRequest +import android.webkit.WebResourceRequest +import com.acsbendi.requestinspectorwebview.WebViewRequest + +class RequestUrlMatcher : RequestMatcher { + private val recordedRequests = ArrayList() + + override fun addRecordedRequest(recordedRequest: RecordedRequest) { + synchronized(recordedRequests) { + recordedRequests.add(recordedRequest) + } + } + + override fun createWebViewRequest(request: WebResourceRequest): WebViewRequest { + val recordedRequest = findRecordedRequest(request) + return WebViewRequest.create(request, recordedRequest) + } + + private fun findRecordedRequest(request: WebResourceRequest): RecordedRequest? { + return synchronized(recordedRequests) { + val url = request.url.toString() + // use findLast instead of find to find the last added query matching a URL - + // they are included at the end of the list when written. + recordedRequests.findLast { recordedRequest -> + // Added search by exact URL to find the actual request body + url == recordedRequest.url + } ?: recordedRequests.findLast { recordedRequest -> + // Previously, there was only a search by contains, and because of this, sometimes the wrong request body was found + url.contains(recordedRequest.url) + } + } + } +} From 25663e9c4ca4c15d3db9cdcb5319b8dedb98b97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Tue, 20 Jan 2026 09:01:37 +0100 Subject: [PATCH 3/6] update dependency versions Since we want to introduce a new dependency on it's newest version, it's time to update all the other dependencies. Therefore we update to Java 17, update the gradle and gradle plugin version and adapted deprecated gradle functionality. This change also requires to replace `targetSdk`, which is removed from the AGP for the `com.android.library` plugin, with a targetSdk for `testOptions` and `lint`. --- app/build.gradle.kts | 11 ++++++++--- build.gradle.kts | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e824808..b595cab 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,7 +15,12 @@ android { defaultConfig { minSdk = 21 - targetSdk = 31 + testOptions { + targetSdk = 31 + } + lint { + targetSdk = 31 + } version = currentVersion } @@ -28,8 +33,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } publishing { diff --git a/build.gradle.kts b/build.gradle.kts index d60edc0..ab0f969 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,8 +6,8 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.4.0") - classpath(kotlin("gradle-plugin", version = "1.6.21")) + classpath("com.android.tools.build:gradle:8.4.2") + classpath(kotlin("gradle-plugin", version = "2.0.21")) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,5 +15,5 @@ buildscript { } tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory.get().asFile) } From e7886d428ef7964b3edde279c09b2ae12e79804a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Tue, 13 Jan 2026 16:57:53 +0100 Subject: [PATCH 4/6] implement uuid matcher Matching by URL doesn't work properly for 2 or more parallel GraphQL queries to the same url. These requests are send to the same url and only differ by the body that is send. If the second request is send to early, before the first request is matched to it's body, the first request will get the body of the second one (because of the `findLast` in the match function) and therefore return something unexpected. The second request will work since it is using the expected body. Therefore a new matcher is required. It will generate a uuid, add it as a header to the request, before sending it. This way the request and body can be properly matched. A downside of this approach is that neither form submission nor CORS requests will work. The reason for this is that the browser engine doesn't allow to add custom headers for from submissions. Regarding CORS: For XHR and fetch the browser engine knows about the additional header and adds it as a value to the preflight `Access-Control-Request-Headers` header, but even though the preflight is successful, it doesn't return that header as allowed. So the browser engine won't send the CORS request but let's it fail. Cleaning up the headers when intercepting the request (and matching it to it's body) doesn't work, because the browser engine doesn't know about the cleanup and, still thinking the header is not allowed, blocks the CORS request. Therefore this solution checks if it's a request to the same origin before adding the header. --- app/build.gradle.kts | 4 + .../RequestInspectorJavaScriptInterface.kt | 47 ++++++++--- .../RequestInspectorWebViewClient.kt | 2 + .../matcher/RequestMatcher.kt | 3 + .../matcher/RequestUrlMatcher.kt | 4 +- .../matcher/RequestUuidMatcher.kt | 84 +++++++++++++++++++ 6 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b595cab..7d176c7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,3 +57,7 @@ publishing { } } } + +dependencies { + implementation("androidx.core:core-ktx:1.17.0") +} diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt index 7bc3a80..89745e7 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -1,9 +1,11 @@ package com.acsbendi.requestinspectorwebview +import android.net.Uri import android.util.Log import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebView +import androidx.core.net.toUri import com.acsbendi.requestinspectorwebview.matcher.RequestMatcher import org.intellij.lang.annotations.Language import org.json.JSONArray @@ -23,7 +25,7 @@ class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: Request data class RecordedRequest( val type: WebViewRequestType, - val url: String, + val url: Uri, val method: String, val body: String, val formParameters: Map, @@ -71,7 +73,7 @@ class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: Request addRecordedRequest( RecordedRequest( WebViewRequestType.FORM, - url, + url.toUri(), method, body, formParameterMap, @@ -89,7 +91,7 @@ class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: Request addRecordedRequest( RecordedRequest( WebViewRequestType.XML_HTTP, - url, + url.toUri(), method, body, mapOf(), @@ -107,7 +109,7 @@ class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: Request addRecordedRequest( RecordedRequest( WebViewRequestType.FETCH, - url, + url.toUri(), method, body, mapOf(), @@ -118,6 +120,11 @@ class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: Request ) } + @JavascriptInterface + fun getAdditionalHeaders(url: String): String { + return matcher.additionalHeaders(url).toString() + } + private fun addRecordedRequest(recordedRequest: RecordedRequest) { matcher.addRecordedRequest(recordedRequest) } @@ -309,6 +316,15 @@ XMLHttpRequest.prototype._send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { const err = new Error(); const url = getFullUrl(xmlhttpRequestUrl); + // Inject headers from Kotlin if any + try { + var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); + for (var h in extraHeaders) { + if (extraHeaders.hasOwnProperty(h)) { + this.setRequestHeader(h, extraHeaders[h]); + } + } + } catch (e) { console.warn('Failed to inject headers from Kotlin (XHR):', e); } $INTERFACE_NAME.recordXhr( url, lastXmlhttpRequestPrototypeMethod, @@ -325,22 +341,33 @@ XMLHttpRequest.prototype.send = function (body) { window._fetch = window.fetch; window.fetch = function () { const firstArgument = arguments[0]; - let url; - let method; - let body; - let headers; + let url, method, body, headers; if (typeof firstArgument === 'string') { url = firstArgument; + if (!arguments[1]) arguments[1] = {}; method = arguments[1] && 'method' in arguments[1] ? arguments[1]['method'] : "GET"; body = arguments[1] && 'body' in arguments[1] ? arguments[1]['body'] : ""; - headers = JSON.stringify(arguments[1] && 'headers' in arguments[1] ? arguments[1]['headers'] : {}); + headers = arguments[1] && 'headers' in arguments[1] ? arguments[1]['headers'] : {}; + // Inject headers from Kotlin if any + try { + var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); + arguments[1].headers = Object.assign({}, extraHeaders, headers || {}); + } catch (e) { console.warn('Failed to inject headers from Kotlin (fetch):', e); } } else { // Request object url = firstArgument.url; method = firstArgument.method; body = firstArgument.body; - headers = JSON.stringify(Object.fromEntries(firstArgument.headers.entries())); + headers = Object.fromEntries(firstArgument.headers.entries()); + // Inject headers from Kotlin if any + try { + var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); + for (var h in extraHeaders) { + firstArgument.headers.set ? firstArgument.headers.set(h, extraHeaders[h]) : firstArgument.headers[h] = extraHeaders[h]; + } + } catch (e) { console.warn('Failed to inject headers from Kotlin (fetch):', e); } } + const fullUrl = getFullUrl(url); const err = new Error(); $INTERFACE_NAME.recordFetch(fullUrl, method, body, headers, err.stack); diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt index ff3b11b..f764c10 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt @@ -7,6 +7,7 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient +import androidx.core.net.toUri import com.acsbendi.requestinspectorwebview.matcher.RequestMatcher import com.acsbendi.requestinspectorwebview.matcher.RequestUrlMatcher @@ -47,6 +48,7 @@ open class RequestInspectorWebViewClient @JvmOverloads constructor( override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { Log.i(LOG_TAG, "Page started loading, enabling request inspection. URL: $url") + matcher.setOrigin(url) RequestInspectorJavaScriptInterface.enabledRequestInspection( view, options.extraJavaScriptToInject diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt index f2d2880..db79ffa 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt @@ -3,9 +3,12 @@ package com.acsbendi.requestinspectorwebview.matcher import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface.RecordedRequest import android.webkit.WebResourceRequest import com.acsbendi.requestinspectorwebview.WebViewRequest +import org.json.JSONObject interface RequestMatcher { fun addRecordedRequest(recordedRequest: RecordedRequest) fun createWebViewRequest(request: WebResourceRequest): WebViewRequest + fun additionalHeaders(url: String): JSONObject = JSONObject() + fun setOrigin(url: String) {} } diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt index f5eda74..56151ef 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUrlMatcher.kt @@ -25,10 +25,10 @@ class RequestUrlMatcher : RequestMatcher { // they are included at the end of the list when written. recordedRequests.findLast { recordedRequest -> // Added search by exact URL to find the actual request body - url == recordedRequest.url + url == recordedRequest.url.toString() } ?: recordedRequests.findLast { recordedRequest -> // Previously, there was only a search by contains, and because of this, sometimes the wrong request body was found - url.contains(recordedRequest.url) + url.contains(recordedRequest.url.toString()) } } } diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt new file mode 100644 index 0000000..1ec005f --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt @@ -0,0 +1,84 @@ +package com.acsbendi.requestinspectorwebview.matcher + +import android.net.Uri +import android.util.Log +import android.webkit.WebResourceRequest +import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface.RecordedRequest +import com.acsbendi.requestinspectorwebview.WebViewRequest +import org.json.JSONObject +import java.util.UUID +import androidx.core.net.toUri + +/** + * This matcher only works for NON CORS requests. It adds a unique UUID header to each request + * originating from the WebView, and matches recorded requests based on that header. + * + * It doesn't work for CORS requests, because it changes the headers of the request, which influences the preflight + * request checking for allowed headers. Even when cleaning up the headers after the request is matched with it's body, + * the CORS request will fail because the browser engine only knows about the adapted header and doesn't execute the + * CORS request, because the preflight check doesn't return the custom header as allowed. + */ +class RequestUuidMatcher() : RequestMatcher { + + private val recordedRequests = mutableMapOf() + private var origin: String = "" + + override fun addRecordedRequest(recordedRequest: RecordedRequest) { + val id = getUuidFromRequest(recordedRequest) ?: return + + synchronized(recordedRequests) { + recordedRequests[id] = recordedRequest + } + } + + private fun getUuidFromRequest(recordedRequest: RecordedRequest): String? = + recordedRequest.headers[REQUEST_ID_HEADER] + + override fun createWebViewRequest(request: WebResourceRequest): WebViewRequest { + val recordedRequest = findRecordedRequest(request) + val (cleanedRequest, cleanedRecordedRequest) = cleanupRequests(request, recordedRequest) + return WebViewRequest.create(cleanedRequest, cleanedRecordedRequest) + } + + private fun cleanupRequests(request: WebResourceRequest, recordedRequest: RecordedRequest?): Pair { + // Clean up headers by removing REQUEST_ID_HEADER from both requests + val cleanedRequest = object : WebResourceRequest by request { + override fun getRequestHeaders(): Map = + request.requestHeaders.filter { (key, _) -> key != REQUEST_ID_HEADER } + } + val cleanedRecordedRequest = recordedRequest?.copy( + headers = recordedRequest.headers.filter { (key, _) -> key != REQUEST_ID_HEADER } + ) + return cleanedRequest to cleanedRecordedRequest + } + + private fun findRecordedRequest(request: WebResourceRequest): RecordedRequest? { + val id = request.requestHeaders[REQUEST_ID_HEADER] ?: return null + val recordedRequest = synchronized(recordedRequests) { + recordedRequests.remove(id) + } + return recordedRequest + } + + override fun additionalHeaders(url: String): JSONObject { + val headersJson = JSONObject() + if (url.startsWith(origin)) { + headersJson.put(REQUEST_ID_HEADER, UUID.randomUUID().toString()) + } else { + Log.i(LOG_TAG, "Recorded CORS to $url, not adding $REQUEST_ID_HEADER") + } + return headersJson + } + + override fun setOrigin(url: String) { + val uri = url.toUri() + val port = if (uri.port != -1) ":${uri.port}" else "" + origin = "${uri.scheme}://${uri.host}$port" + + } + + companion object { + private const val LOG_TAG = "RequestUuidMatcher" + private const val REQUEST_ID_HEADER = "x-request-inspector-id" + } +} From 466577b5f48abda9c4bfbf37cb11b473872dda81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Tue, 3 Mar 2026 08:32:32 +0100 Subject: [PATCH 5/6] implement uuid matcher Matching by URL doesn't work properly for 2 or more parallel GraphQL queries to the same url. These requests are send to the same url and only differ by the body that is send. If the second request is send to early, before the first request is matched to it's body, the first request will get the body of the second one (because of the `findLast` in the match function) and therefore return something unexpected. The second request will work since it is using the expected body. Therefore a new matcher is required. It will generate a uuid, add it as a header to the request, before sending it. This way the request and body can be properly matched. A downside of this approach is that neither form submission nor CORS requests will work. The reason for this is that the browser engine doesn't allow to add custom headers for from submissions. Regarding CORS: For XHR and fetch the browser engine knows about the additional header and adds it as a value to the preflight `Access-Control-Request-Headers` header, but even though the preflight is successful, it doesn't return that header as allowed. So the browser engine won't send the CORS request but let's it fail. Cleaning up the headers when intercepting the request (and matching it to it's body) doesn't work, because the browser engine doesn't know about the cleanup and, still thinking the header is not allowed, blocks the CORS request. Therefore this solution checks if it's a request to the same origin before adding the header. --- .../RequestInspectorJavaScriptInterface.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt index 89745e7..729939d 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -324,7 +324,9 @@ XMLHttpRequest.prototype.send = function (body) { this.setRequestHeader(h, extraHeaders[h]); } } - } catch (e) { console.warn('Failed to inject headers from Kotlin (XHR):', e); } + } catch (e) { + console.warn('Failed to inject headers from Kotlin (XHR):', e); + } $INTERFACE_NAME.recordXhr( url, lastXmlhttpRequestPrototypeMethod, @@ -352,7 +354,9 @@ window.fetch = function () { try { var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); arguments[1].headers = Object.assign({}, extraHeaders, headers || {}); - } catch (e) { console.warn('Failed to inject headers from Kotlin (fetch):', e); } + } catch (e) { + console.warn('Failed to inject headers from Kotlin (fetch):', e); + } } else { // Request object url = firstArgument.url; @@ -365,7 +369,9 @@ window.fetch = function () { for (var h in extraHeaders) { firstArgument.headers.set ? firstArgument.headers.set(h, extraHeaders[h]) : firstArgument.headers[h] = extraHeaders[h]; } - } catch (e) { console.warn('Failed to inject headers from Kotlin (fetch):', e); } + } catch (e) { + console.warn('Failed to inject headers from Kotlin (fetch):', e); + } } const fullUrl = getFullUrl(url); From 79afa72f27d4f2c450da818cc685e31b15c8f36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Mon, 19 Jan 2026 15:12:04 +0100 Subject: [PATCH 6/6] implement uuid in query param matcher The old logic with the uuid in a custom header didn't work for CORS requests, but adding the uuid as a query param should work. Downside is that the uuid will look different in the web inspector. So we restructure the RequestUuidMatcher into an abstract class, with two implementations RequestUuidInHeaderMatcher.kt (with the previous logic) and RequestUuidInQueryParamMatcher.kt with the new logic. Then the user of the library can decide which one to use. --- README.md | 22 +++++ .../RequestInspectorJavaScriptInterface.kt | 78 ++++++++++------- .../RequestInspectorWebViewClient.kt | 6 +- .../RequestGeneratedUuidInHeaderMatcher.kt | 61 ++++++++++++++ ...RequestGeneratedUuidInQueryParamMatcher.kt | 39 +++++++++ .../matcher/RequestGeneratedUuidMatcher.kt | 47 +++++++++++ .../matcher/RequestMatcher.kt | 5 +- .../matcher/RequestUuidMatcher.kt | 84 ------------------- 8 files changed, 224 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInHeaderMatcher.kt create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInQueryParamMatcher.kt create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidMatcher.kt delete mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt diff --git a/README.md b/README.md index c02bbe1..c76f324 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,28 @@ To manually process requests: } ``` +For both cases you can choose between different strategies for how the recorded requests (including +the body) and the intercepted requests are matched together. By default, only the url is used. If +you want to use a different strategy, for example if you have parallel requests to the same url with +different bodies (e.g. GraphQL queries), you can pass a custom `RequestMatcher` to the constructor +of `RequestInspectorWebViewClient`: + +```kotlin + val webView = WebView(this) + webView.webViewClient = RequestInspectorWebViewClient( + webView, + matcher = RequestGeneratedUuidInHeaderMatcher() + ) +``` + +Currently available matchers are `RequestGeneratedUuidInHeaderMatcher` and +`RequestGeneratedUuidInUrlMatcher`, which both create an UUID and add it to the request before it's +recorded and sent. They only differ by how they attach the UUID to the request, as an additional +header or as an additional query param. But both clean up the request before it's been sent. + +If you want to implement your own matching strategy, you can implement the `RequestMatcher` +interface and pass an instance of it to the constructor of `RequestInspectorWebViewClient`. + Known limitations === diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt index 729939d..92505a7 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -122,7 +122,12 @@ class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: Request @JavascriptInterface fun getAdditionalHeaders(url: String): String { - return matcher.additionalHeaders(url).toString() + return matcher.getAdditionalHeaders(url).toString() + } + + @JavascriptInterface + fun getAdditionalQueryParam(): String { + return matcher.getAdditionalQueryParams() } private fun addRecordedRequest(recordedRequest: RecordedRequest) { @@ -251,6 +256,31 @@ function getFullUrl(url) { } } +function setAdditionalHeaders(url, callback) { + try { + var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); + callback(extraHeaders); + } catch (e) { + console.warn('Failed to inject headers from Kotlin:', e); + } +} + +function appendAdditionalQueryParams(url) { + try { + var extraQueryParam = $INTERFACE_NAME.getAdditionalQueryParam(); + if (extraQueryParam) { + if (url.indexOf('?') === -1) { + url += '?' + extraQueryParam; + } else { + url += '&' + extraQueryParam; + } + } + } catch (e) { + console.warn('Failed to inject query param from Kotlin:', e); + } + return url; +} + function recordFormSubmission(form) { var jsonArr = []; for (i = 0; i < form.elements.length; i++) { @@ -271,7 +301,7 @@ function recordFormSubmission(form) { const path = form.attributes['action'] === undefined ? "/" : form.attributes['action'].nodeValue; const method = form.attributes['method'] === undefined ? "GET" : form.attributes['method'].nodeValue; - const url = getFullUrl(path); + const url = appendAdditionalQueryParams(getFullUrl(path)); const encType = form.attributes['enctype'] === undefined ? "application/x-www-form-urlencoded" : form.attributes['enctype'].nodeValue; const err = new Error(); $INTERFACE_NAME.recordFormSubmission( @@ -303,9 +333,9 @@ let xmlhttpRequestUrl = null; XMLHttpRequest.prototype._open = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, async, user, password) { lastXmlhttpRequestPrototypeMethod = method; - xmlhttpRequestUrl = url; + xmlhttpRequestUrl = appendAdditionalQueryParams(url); const asyncWithDefault = async === undefined ? true : async; - this._open(method, url, asyncWithDefault, user, password); + this._open(method, xmlhttpRequestUrl, asyncWithDefault, user, password); }; XMLHttpRequest.prototype._setRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (header, value) { @@ -315,18 +345,14 @@ XMLHttpRequest.prototype.setRequestHeader = function (header, value) { XMLHttpRequest.prototype._send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { const err = new Error(); - const url = getFullUrl(xmlhttpRequestUrl); - // Inject headers from Kotlin if any - try { - var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); + let url = getFullUrl(xmlhttpRequestUrl); + setAdditionalHeaders(url, function(extraHeaders) { for (var h in extraHeaders) { if (extraHeaders.hasOwnProperty(h)) { this.setRequestHeader(h, extraHeaders[h]); } } - } catch (e) { - console.warn('Failed to inject headers from Kotlin (XHR):', e); - } + }.bind(this)); $INTERFACE_NAME.recordXhr( url, lastXmlhttpRequestPrototypeMethod, @@ -345,33 +371,27 @@ window.fetch = function () { const firstArgument = arguments[0]; let url, method, body, headers; if (typeof firstArgument === 'string') { - url = firstArgument; + url = appendAdditionalQueryParams(firstArgument); if (!arguments[1]) arguments[1] = {}; - method = arguments[1] && 'method' in arguments[1] ? arguments[1]['method'] : "GET"; - body = arguments[1] && 'body' in arguments[1] ? arguments[1]['body'] : ""; - headers = arguments[1] && 'headers' in arguments[1] ? arguments[1]['headers'] : {}; - // Inject headers from Kotlin if any - try { - var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); - arguments[1].headers = Object.assign({}, extraHeaders, headers || {}); - } catch (e) { - console.warn('Failed to inject headers from Kotlin (fetch):', e); - } + method = 'method' in arguments[1] ? arguments[1]['method'] : "GET"; + body = 'body' in arguments[1] ? arguments[1]['body'] : ""; + headers = 'headers' in arguments[1] ? arguments[1]['headers'] : {}; + setAdditionalHeaders(url, function(extraHeaders) { + arguments[1].headers = { ...extraHeaders, ...headers }; + }); + arguments[0] = url; } else { // Request object - url = firstArgument.url; + url = appendAdditionalQueryParams(firstArgument.url); method = firstArgument.method; body = firstArgument.body; headers = Object.fromEntries(firstArgument.headers.entries()); - // Inject headers from Kotlin if any - try { - var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); + setAdditionalHeaders(url, function(extraHeaders) { for (var h in extraHeaders) { firstArgument.headers.set ? firstArgument.headers.set(h, extraHeaders[h]) : firstArgument.headers[h] = extraHeaders[h]; } - } catch (e) { - console.warn('Failed to inject headers from Kotlin (fetch):', e); - } + }); + firstArgument.url = url; } const fullUrl = getFullUrl(url); diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt index f764c10..aab9f68 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorWebViewClient.kt @@ -7,13 +7,13 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient -import androidx.core.net.toUri import com.acsbendi.requestinspectorwebview.matcher.RequestMatcher import com.acsbendi.requestinspectorwebview.matcher.RequestUrlMatcher @SuppressLint("SetJavaScriptEnabled") open class RequestInspectorWebViewClient @JvmOverloads constructor( - webView: WebView, val matcher: RequestMatcher = RequestUrlMatcher(), + webView: WebView, + val matcher: RequestMatcher = RequestUrlMatcher(), private val options: RequestInspectorOptions = RequestInspectorOptions() ) : WebViewClient() { @@ -48,7 +48,7 @@ open class RequestInspectorWebViewClient @JvmOverloads constructor( override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { Log.i(LOG_TAG, "Page started loading, enabling request inspection. URL: $url") - matcher.setOrigin(url) + matcher.onPageStarted(url) RequestInspectorJavaScriptInterface.enabledRequestInspection( view, options.extraJavaScriptToInject diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInHeaderMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInHeaderMatcher.kt new file mode 100644 index 0000000..10c1652 --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInHeaderMatcher.kt @@ -0,0 +1,61 @@ +package com.acsbendi.requestinspectorwebview.matcher + +import android.util.Log +import android.webkit.WebResourceRequest +import androidx.core.net.toUri +import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface.RecordedRequest +import org.json.JSONObject +import java.util.UUID + +/** + * This matcher only works for NON CORS requests. It adds a unique UUID header to each request + * originating from the WebView, and matches recorded requests based on that header. + * + * It doesn't work for CORS requests, because it changes the headers of the request, which influences the preflight + * request checking for allowed headers. Even when cleaning up the headers after the request is matched with it's body, + * the CORS request will fail because the browser engine only knows about the adapted header and doesn't execute the + * CORS request, because the preflight check doesn't return the custom header as allowed. + */ +class RequestGeneratedUuidInHeaderMatcher() : RequestGeneratedUuidMatcher() { + + private var origin: String = "" + + override fun getUuidFromRequest(recordedRequest: RecordedRequest): String? = + recordedRequest.headers[REQUEST_INSPECTOR_ID] + + override fun getUuidFromRequest(webResourceRequest: WebResourceRequest): String? = + webResourceRequest.requestHeaders[REQUEST_INSPECTOR_ID] + + override fun removeUuidFromRequests(request: WebResourceRequest, recordedRequest: RecordedRequest?): Pair { + // Clean up headers by removing REQUEST_ID_HEADER from both requests + val cleanedRequest = object : WebResourceRequest by request { + override fun getRequestHeaders(): Map = + request.requestHeaders.filter { (key, _) -> key != REQUEST_INSPECTOR_ID } + } + val cleanedRecordedRequest = recordedRequest?.copy( + headers = recordedRequest.headers.filter { (key, _) -> key != REQUEST_INSPECTOR_ID } + ) + return cleanedRequest to cleanedRecordedRequest + } + + override fun getAdditionalHeaders(url: String): JSONObject { + val headersJson = JSONObject() + if (getOrigin(url) == origin) { + val uuid = UUID.randomUUID().toString() + headersJson.put(REQUEST_INSPECTOR_ID, uuid) + } else { + Log.i(LOG_TAG, "Recorded CORS to $url, not adding $REQUEST_INSPECTOR_ID") + } + return headersJson + } + + override fun onPageStarted(url: String) { + origin = getOrigin(url) + } + + private fun getOrigin(url: String): String { + val uri = url.toUri() + val port = if (uri.port != -1) ":${uri.port}" else "" + return "${uri.scheme}://${uri.host}$port" + } +} diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInQueryParamMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInQueryParamMatcher.kt new file mode 100644 index 0000000..507b967 --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidInQueryParamMatcher.kt @@ -0,0 +1,39 @@ +package com.acsbendi.requestinspectorwebview.matcher + +import android.webkit.WebResourceRequest +import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface +import java.util.UUID + +class RequestGeneratedUuidInQueryParamMatcher : RequestGeneratedUuidMatcher() { + + override fun getUuidFromRequest(recordedRequest: RequestInspectorJavaScriptInterface.RecordedRequest): String? = + recordedRequest.url.getQueryParameter(REQUEST_INSPECTOR_ID) + + override fun getUuidFromRequest(webResourceRequest: WebResourceRequest): String? = + webResourceRequest.url.getQueryParameter(REQUEST_INSPECTOR_ID) + + override fun removeUuidFromRequests( + request: WebResourceRequest, + recordedRequest: RequestInspectorJavaScriptInterface.RecordedRequest? + ): Pair { + val originalUrl = request.url + val cleanedUrlBuilder = originalUrl.buildUpon().clearQuery() + for (key in originalUrl.queryParameterNames.filter { it != REQUEST_INSPECTOR_ID }) { + originalUrl.getQueryParameters(key).forEach { paramValue -> + cleanedUrlBuilder.appendQueryParameter(key, paramValue) + } + } + val cleanedUrl = cleanedUrlBuilder.build() + + val cleanedWebResourceRequest = object : WebResourceRequest by request { + override fun getUrl() = cleanedUrl + } + val cleanedRecordedRequest = recordedRequest?.copy(url = cleanedUrl) + return cleanedWebResourceRequest to cleanedRecordedRequest + } + + override fun getAdditionalQueryParams(): String { + val uuid = UUID.randomUUID().toString() + return "$REQUEST_INSPECTOR_ID=$uuid" + } +} diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidMatcher.kt new file mode 100644 index 0000000..ef8384d --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestGeneratedUuidMatcher.kt @@ -0,0 +1,47 @@ +package com.acsbendi.requestinspectorwebview.matcher + +import android.webkit.WebResourceRequest +import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface.RecordedRequest +import com.acsbendi.requestinspectorwebview.WebViewRequest + +abstract class RequestGeneratedUuidMatcher : RequestMatcher { + + private val recordedRequests = mutableMapOf() + + abstract fun getUuidFromRequest(recordedRequest: RecordedRequest): String? + abstract fun getUuidFromRequest(webResourceRequest: WebResourceRequest): String? + abstract fun removeUuidFromRequests( + request: WebResourceRequest, + recordedRequest: RecordedRequest? + ): Pair + + final override fun addRecordedRequest(recordedRequest: RecordedRequest) { + val id = getUuidFromRequest(recordedRequest) ?: return + + synchronized(recordedRequests) { + recordedRequests[id] = recordedRequest + } + } + + override fun createWebViewRequest(request: WebResourceRequest): WebViewRequest { + val recordedRequest = findRecordedRequest(request) + val (cleanedRequest, cleanedRecordedRequest) = removeUuidFromRequests(request, recordedRequest) + return WebViewRequest.create(cleanedRequest, cleanedRecordedRequest) + } + + + private fun findRecordedRequest(request: WebResourceRequest): RecordedRequest? { + val id = getUuidFromRequest(request) ?: return null + val recordedRequest = synchronized(recordedRequests) { + recordedRequests.remove(id) + } + return recordedRequest + } + + override fun onPageStarted(url: String) {} + + companion object { + const val REQUEST_INSPECTOR_ID = "x-request-inspector-id" + const val LOG_TAG = "RequestGeneratedUuidMatcher" + } +} diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt index db79ffa..73e7e6f 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt @@ -8,7 +8,8 @@ import org.json.JSONObject interface RequestMatcher { fun addRecordedRequest(recordedRequest: RecordedRequest) fun createWebViewRequest(request: WebResourceRequest): WebViewRequest - fun additionalHeaders(url: String): JSONObject = JSONObject() - fun setOrigin(url: String) {} + fun getAdditionalHeaders(url: String): JSONObject = JSONObject() + fun getAdditionalQueryParams(): String = "" + fun onPageStarted(url: String) {} } diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt deleted file mode 100644 index 1ec005f..0000000 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.acsbendi.requestinspectorwebview.matcher - -import android.net.Uri -import android.util.Log -import android.webkit.WebResourceRequest -import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface.RecordedRequest -import com.acsbendi.requestinspectorwebview.WebViewRequest -import org.json.JSONObject -import java.util.UUID -import androidx.core.net.toUri - -/** - * This matcher only works for NON CORS requests. It adds a unique UUID header to each request - * originating from the WebView, and matches recorded requests based on that header. - * - * It doesn't work for CORS requests, because it changes the headers of the request, which influences the preflight - * request checking for allowed headers. Even when cleaning up the headers after the request is matched with it's body, - * the CORS request will fail because the browser engine only knows about the adapted header and doesn't execute the - * CORS request, because the preflight check doesn't return the custom header as allowed. - */ -class RequestUuidMatcher() : RequestMatcher { - - private val recordedRequests = mutableMapOf() - private var origin: String = "" - - override fun addRecordedRequest(recordedRequest: RecordedRequest) { - val id = getUuidFromRequest(recordedRequest) ?: return - - synchronized(recordedRequests) { - recordedRequests[id] = recordedRequest - } - } - - private fun getUuidFromRequest(recordedRequest: RecordedRequest): String? = - recordedRequest.headers[REQUEST_ID_HEADER] - - override fun createWebViewRequest(request: WebResourceRequest): WebViewRequest { - val recordedRequest = findRecordedRequest(request) - val (cleanedRequest, cleanedRecordedRequest) = cleanupRequests(request, recordedRequest) - return WebViewRequest.create(cleanedRequest, cleanedRecordedRequest) - } - - private fun cleanupRequests(request: WebResourceRequest, recordedRequest: RecordedRequest?): Pair { - // Clean up headers by removing REQUEST_ID_HEADER from both requests - val cleanedRequest = object : WebResourceRequest by request { - override fun getRequestHeaders(): Map = - request.requestHeaders.filter { (key, _) -> key != REQUEST_ID_HEADER } - } - val cleanedRecordedRequest = recordedRequest?.copy( - headers = recordedRequest.headers.filter { (key, _) -> key != REQUEST_ID_HEADER } - ) - return cleanedRequest to cleanedRecordedRequest - } - - private fun findRecordedRequest(request: WebResourceRequest): RecordedRequest? { - val id = request.requestHeaders[REQUEST_ID_HEADER] ?: return null - val recordedRequest = synchronized(recordedRequests) { - recordedRequests.remove(id) - } - return recordedRequest - } - - override fun additionalHeaders(url: String): JSONObject { - val headersJson = JSONObject() - if (url.startsWith(origin)) { - headersJson.put(REQUEST_ID_HEADER, UUID.randomUUID().toString()) - } else { - Log.i(LOG_TAG, "Recorded CORS to $url, not adding $REQUEST_ID_HEADER") - } - return headersJson - } - - override fun setOrigin(url: String) { - val uri = url.toUri() - val port = if (uri.port != -1) ":${uri.port}" else "" - origin = "${uri.scheme}://${uri.host}$port" - - } - - companion object { - private const val LOG_TAG = "RequestUuidMatcher" - private const val REQUEST_ID_HEADER = "x-request-inspector-id" - } -}