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/8] 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/8] 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/8] 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/8] 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 74db322f587ded453a387c1308fa103c888dddba 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 5/8] 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. --- .../RequestInspectorJavaScriptInterface.kt | 33 ++++++++-- .../matcher/RequestMatcher.kt | 1 + .../matcher/RequestUuidInHeaderMatcher.kt | 61 ++++++++++++++++++ .../matcher/RequestUuidInQueryParamMatcher.kt | 39 ++++++++++++ .../matcher/RequestUuidMatcher.kt | 62 ++++--------------- 5 files changed, 140 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt create mode 100644 app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt index 89745e7..9632d1d 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -125,6 +125,11 @@ class RequestInspectorJavaScriptInterface(webView: WebView, val matcher: Request return matcher.additionalHeaders(url).toString() } + @JavascriptInterface + fun getAdditionalQueryParam(): String { + return matcher.getAdditionalQueryParam() + } + private fun addRecordedRequest(recordedRequest: RecordedRequest) { matcher.addRecordedRequest(recordedRequest) } @@ -251,6 +256,20 @@ function getFullUrl(url) { } } +function appendAdditionalQueryParam(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 +290,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 = appendAdditionalQueryParam(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 +322,9 @@ let xmlhttpRequestUrl = null; XMLHttpRequest.prototype._open = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, async, user, password) { lastXmlhttpRequestPrototypeMethod = method; - xmlhttpRequestUrl = url; + xmlhttpRequestUrl = appendAdditionalQueryParam(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,7 +334,7 @@ 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); + let url = getFullUrl(xmlhttpRequestUrl); // Inject headers from Kotlin if any try { var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url)); @@ -343,7 +362,7 @@ window.fetch = function () { const firstArgument = arguments[0]; let url, method, body, headers; if (typeof firstArgument === 'string') { - url = firstArgument; + url = appendAdditionalQueryParam(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'] : ""; @@ -353,9 +372,10 @@ window.fetch = function () { 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); } + arguments[0] = url; } else { // Request object - url = firstArgument.url; + url = appendAdditionalQueryParam(firstArgument.url); method = firstArgument.method; body = firstArgument.body; headers = Object.fromEntries(firstArgument.headers.entries()); @@ -366,6 +386,7 @@ window.fetch = function () { 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/matcher/RequestMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt index db79ffa..6ea6eff 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt @@ -9,6 +9,7 @@ interface RequestMatcher { fun addRecordedRequest(recordedRequest: RecordedRequest) fun createWebViewRequest(request: WebResourceRequest): WebViewRequest fun additionalHeaders(url: String): JSONObject = JSONObject() + fun getAdditionalQueryParam(): String = "" fun setOrigin(url: String) {} } diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt new file mode 100644 index 0000000..ca7cf16 --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.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 RequestUuidInHeaderMatcher() : RequestUuidMatcher() { + + 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 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_INSPECTOR_ID } + } + val cleanedRecordedRequest = recordedRequest?.copy( + headers = recordedRequest.headers.filter { (key, _) -> key != REQUEST_INSPECTOR_ID } + ) + return cleanedRequest to cleanedRecordedRequest + } + + override fun additionalHeaders(url: String): JSONObject { + val headersJson = JSONObject() + if (url.startsWith(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 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" + } +} diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt new file mode 100644 index 0000000..5862f68 --- /dev/null +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt @@ -0,0 +1,39 @@ +package com.acsbendi.requestinspectorwebview.matcher + +import android.webkit.WebResourceRequest +import com.acsbendi.requestinspectorwebview.RequestInspectorJavaScriptInterface +import java.util.UUID + +class RequestUuidInQueryParamMatcher : RequestUuidMatcher() { + + 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 cleanupRequests( + 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 getAdditionalQueryParam(): String { + val uuid = UUID.randomUUID().toString() + return "$REQUEST_INSPECTOR_ID=$uuid" + } +} diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt index 1ec005f..6601a2c 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt @@ -1,29 +1,21 @@ 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 { +abstract class RequestUuidMatcher : RequestMatcher { private val recordedRequests = mutableMapOf() - private var origin: String = "" - override fun addRecordedRequest(recordedRequest: RecordedRequest) { + abstract fun getUuidFromRequest(recordedRequest: RecordedRequest): String? + abstract fun getUuidFromRequest(webResourceRequest: WebResourceRequest): String? + abstract fun cleanupRequests( + request: WebResourceRequest, + recordedRequest: RecordedRequest? + ): Pair + + final override fun addRecordedRequest(recordedRequest: RecordedRequest) { val id = getUuidFromRequest(recordedRequest) ?: return synchronized(recordedRequests) { @@ -31,54 +23,24 @@ class RequestUuidMatcher() : RequestMatcher { } } - 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 id = getUuidFromRequest(request) ?: 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" - - } + override fun setOrigin(url: String) {} companion object { - private const val LOG_TAG = "RequestUuidMatcher" - private const val REQUEST_ID_HEADER = "x-request-inspector-id" + const val REQUEST_INSPECTOR_ID = "x-request-inspector-id" } } From 34011a226589964cf09ec76b055f4d6a98981707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Mon, 26 Jan 2026 12:23:36 +0100 Subject: [PATCH 6/8] replace `startsWith` origin check by a safe one The `startWith` check could return false positives. E.g. if the origin is `https://example.com` and we check a request to `https://example.com.evil.org`, it would return true. This shouldn't be a security issue (in contrast to the evil url in the example). But it could simply wrongfully miss a CORS request, which would then not be executed because of the additional, not allowed header. --- .../matcher/RequestUuidInHeaderMatcher.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt index ca7cf16..b8f19eb 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt @@ -40,7 +40,7 @@ class RequestUuidInHeaderMatcher() : RequestUuidMatcher() { override fun additionalHeaders(url: String): JSONObject { val headersJson = JSONObject() - if (url.startsWith(origin)) { + if (getOrigin(url) == origin) { val uuid = UUID.randomUUID().toString() headersJson.put(REQUEST_INSPECTOR_ID, uuid) } else { @@ -50,9 +50,13 @@ class RequestUuidInHeaderMatcher() : RequestUuidMatcher() { } override fun setOrigin(url: String) { + origin = getOrigin(url) + } + + private fun getOrigin(url: String): String { val uri = url.toUri() val port = if (uri.port != -1) ":${uri.port}" else "" - origin = "${uri.scheme}://${uri.host}$port" + return "${uri.scheme}://${uri.host}$port" } companion object { From 32875fc6e3f25decc3456274d144d38dc8862d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Tue, 17 Feb 2026 20:47:08 +0100 Subject: [PATCH 7/8] fixup! implement uuid matcher --- .../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 9632d1d..d6a3e51 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -343,7 +343,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, @@ -371,7 +373,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); + } arguments[0] = url; } else { // Request object @@ -385,7 +389,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); + } firstArgument.url = url; } From 3bee8ab305a74876fe4a83960478b5cedac84942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Me=C3=9Fmer?= Date: Tue, 17 Feb 2026 20:47:48 +0100 Subject: [PATCH 8/8] fixup! implement uuid in query param matcher --- README.md | 22 +++++++++++++++++++ .../RequestInspectorJavaScriptInterface.kt | 18 ++++++++------- .../RequestInspectorWebViewClient.kt | 6 ++--- .../matcher/RequestMatcher.kt | 6 ++--- .../matcher/RequestUuidInHeaderMatcher.kt | 6 ++--- .../matcher/RequestUuidInQueryParamMatcher.kt | 4 ++-- .../matcher/RequestUuidMatcher.kt | 6 ++--- 7 files changed, 46 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c02bbe1..d2f53d6 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 = RequestUuidInHeaderMatcher() + ) +``` + +Currently available matchers are `RequestUuidInHeaderMatcher` and `RequestUuidInUrlMatcher`, which +both create an UUID and add it to the request before it's recorded and sent. They only differ by how +the 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 d6a3e51..d3c3f30 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/RequestInspectorJavaScriptInterface.kt @@ -122,12 +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.getAdditionalQueryParam() + return matcher.getAdditionalQueryParams() } private fun addRecordedRequest(recordedRequest: RecordedRequest) { @@ -256,7 +256,7 @@ function getFullUrl(url) { } } -function appendAdditionalQueryParam(url) { +function appendAdditionalQueryParams(url) { try { var extraQueryParam = $INTERFACE_NAME.getAdditionalQueryParam(); if (extraQueryParam) { @@ -266,7 +266,9 @@ function appendAdditionalQueryParam(url) { url += '&' + extraQueryParam; } } - } catch (e) { console.warn('Failed to inject query param from Kotlin:', e); } + } catch (e) { + console.warn('Failed to inject query param from Kotlin:', e); + } return url; } @@ -290,7 +292,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 = appendAdditionalQueryParam(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( @@ -322,7 +324,7 @@ let xmlhttpRequestUrl = null; XMLHttpRequest.prototype._open = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, async, user, password) { lastXmlhttpRequestPrototypeMethod = method; - xmlhttpRequestUrl = appendAdditionalQueryParam(url); + xmlhttpRequestUrl = appendAdditionalQueryParams(url); const asyncWithDefault = async === undefined ? true : async; this._open(method, xmlhttpRequestUrl, asyncWithDefault, user, password); }; @@ -364,7 +366,7 @@ window.fetch = function () { const firstArgument = arguments[0]; let url, method, body, headers; if (typeof firstArgument === 'string') { - url = appendAdditionalQueryParam(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'] : ""; @@ -379,7 +381,7 @@ window.fetch = function () { arguments[0] = url; } else { // Request object - url = appendAdditionalQueryParam(firstArgument.url); + url = appendAdditionalQueryParams(firstArgument.url); method = firstArgument.method; body = firstArgument.body; headers = Object.fromEntries(firstArgument.headers.entries()); 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/RequestMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestMatcher.kt index 6ea6eff..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,8 +8,8 @@ import org.json.JSONObject interface RequestMatcher { fun addRecordedRequest(recordedRequest: RecordedRequest) fun createWebViewRequest(request: WebResourceRequest): WebViewRequest - fun additionalHeaders(url: String): JSONObject = JSONObject() - fun getAdditionalQueryParam(): String = "" - 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/RequestUuidInHeaderMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt index b8f19eb..275ca72 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInHeaderMatcher.kt @@ -26,7 +26,7 @@ class RequestUuidInHeaderMatcher() : RequestUuidMatcher() { override fun getUuidFromRequest(webResourceRequest: WebResourceRequest): String? = webResourceRequest.requestHeaders[REQUEST_INSPECTOR_ID] - override fun cleanupRequests(request: WebResourceRequest, recordedRequest: RecordedRequest?): Pair { + 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 = @@ -38,7 +38,7 @@ class RequestUuidInHeaderMatcher() : RequestUuidMatcher() { return cleanedRequest to cleanedRecordedRequest } - override fun additionalHeaders(url: String): JSONObject { + override fun getAdditionalHeaders(url: String): JSONObject { val headersJson = JSONObject() if (getOrigin(url) == origin) { val uuid = UUID.randomUUID().toString() @@ -49,7 +49,7 @@ class RequestUuidInHeaderMatcher() : RequestUuidMatcher() { return headersJson } - override fun setOrigin(url: String) { + override fun onPageStarted(url: String) { origin = getOrigin(url) } diff --git a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt index 5862f68..bdc0807 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidInQueryParamMatcher.kt @@ -12,7 +12,7 @@ class RequestUuidInQueryParamMatcher : RequestUuidMatcher() { override fun getUuidFromRequest(webResourceRequest: WebResourceRequest): String? = webResourceRequest.url.getQueryParameter(REQUEST_INSPECTOR_ID) - override fun cleanupRequests( + override fun removeUuidFromRequests( request: WebResourceRequest, recordedRequest: RequestInspectorJavaScriptInterface.RecordedRequest? ): Pair { @@ -32,7 +32,7 @@ class RequestUuidInQueryParamMatcher : RequestUuidMatcher() { return cleanedWebResourceRequest to cleanedRecordedRequest } - override fun getAdditionalQueryParam(): String { + 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/RequestUuidMatcher.kt b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt index 6601a2c..8e7de8a 100644 --- a/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt +++ b/app/src/main/java/com/acsbendi/requestinspectorwebview/matcher/RequestUuidMatcher.kt @@ -10,7 +10,7 @@ abstract class RequestUuidMatcher : RequestMatcher { abstract fun getUuidFromRequest(recordedRequest: RecordedRequest): String? abstract fun getUuidFromRequest(webResourceRequest: WebResourceRequest): String? - abstract fun cleanupRequests( + abstract fun removeUuidFromRequests( request: WebResourceRequest, recordedRequest: RecordedRequest? ): Pair @@ -25,7 +25,7 @@ abstract class RequestUuidMatcher : RequestMatcher { override fun createWebViewRequest(request: WebResourceRequest): WebViewRequest { val recordedRequest = findRecordedRequest(request) - val (cleanedRequest, cleanedRecordedRequest) = cleanupRequests(request, recordedRequest) + val (cleanedRequest, cleanedRecordedRequest) = removeUuidFromRequests(request, recordedRequest) return WebViewRequest.create(cleanedRequest, cleanedRecordedRequest) } @@ -38,7 +38,7 @@ abstract class RequestUuidMatcher : RequestMatcher { return recordedRequest } - override fun setOrigin(url: String) {} + override fun onPageStarted(url: String) {} companion object { const val REQUEST_INSPECTOR_ID = "x-request-inspector-id"