Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/fix-android-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wry": patch
---

Fix empty custom protocol body on Android.
58 changes: 38 additions & 20 deletions src/android/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ macro_rules! android_binding {
$package,
RustWebViewClient,
handleRequest,
[JString, JObject, jboolean],
[JString, JObject, JString, jboolean],
jobject
);
android_fn!(
Expand Down Expand Up @@ -104,6 +104,7 @@ fn handle_request(
env: &mut JNIEnv,
webview_id: JString,
request: JObject,
body: JString,
is_document_start_script_enabled: jboolean,
) -> JniResult<jobject> {
if let Some(handler) = REQUEST_HANDLER.lock().unwrap().as_ref() {
Expand Down Expand Up @@ -157,11 +158,26 @@ fn handle_request(
}
}

let final_request = match request_builder.body(Vec::new()) {
let body = env.get_string(&body)?;
let body = body.to_str().ok().unwrap_or_default();
let body = match body.chars().next() {
Some('s') => body.chars().skip(1).map(|c| c as u8).collect(),
Some('a') => {
let array_str = body.chars().skip(1).collect::<String>();
array_str
.split(',')
.map(|s| s.parse::<u8>().unwrap())
.collect()
}
None => Vec::new(),
_ => panic!("unexpected Android body format: {body}"),
};

let final_request = match request_builder.body(body) {
Ok(req) => req,
Err(e) => {
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to build response: {}", e);
tracing::warn!("Failed to build response: {_e}");
return Ok(*JObject::null());
}
};
Expand Down Expand Up @@ -190,9 +206,9 @@ fn handle_request(
} else {
None
};
if let Some(err) = status_err {
if let Some(_err) = status_err {
#[cfg(feature = "tracing")]
tracing::warn!("{}", err);
tracing::warn!("{_err}");
return Ok(*JObject::null());
}

Expand Down Expand Up @@ -271,18 +287,20 @@ pub unsafe fn handleRequest(
_: JClass,
webview_id: JString,
request: JObject,
body: JString,
is_document_start_script_enabled: jboolean,
) -> jobject {
match handle_request(
&mut env,
webview_id,
request,
body,
is_document_start_script_enabled,
) {
Ok(response) => response,
Err(e) => {
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to handle request: {}", e);
tracing::warn!("Failed to handle request: {_e}");
JObject::null().as_raw()
}
}
Expand All @@ -304,9 +322,9 @@ pub unsafe fn shouldOverride(mut env: JNIEnv, _: JClass, url: JString) -> jboole
.map(|f| !(f.handler)(url))
.unwrap_or(false)
}
Err(e) => {
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {}", e);
tracing::warn!("Failed to parse JString: {_e}");
false
}
}
Expand All @@ -326,9 +344,9 @@ pub unsafe fn onEval(mut env: JNIEnv, _: JClass, id: jint, result: JString) {
cb(result.into());
}
}
Err(e) => {
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {}", e);
tracing::warn!("Failed to parse JString: {_e}");
}
}
}
Expand All @@ -345,9 +363,9 @@ pub unsafe fn ipc(mut env: JNIEnv, _: JClass, url: JString, body: JString) {
(ipc.handler)(Request::builder().uri(url).body(body).unwrap())
}
}
(Err(e), _) | (_, Err(e)) => {
(Err(_e), _) | (_, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {}", e)
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
Expand All @@ -361,9 +379,9 @@ pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, _webview: JObject,
(title_handler.handler)(title)
}
}
Err(e) => {
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {}", e)
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
Expand Down Expand Up @@ -391,9 +409,9 @@ pub unsafe fn onPageLoading(mut env: JNIEnv, _: JClass, url: JString) {
(on_load.handler)(PageLoadEvent::Started, url)
}
}
Err(e) => {
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {}", e)
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
Expand All @@ -407,9 +425,9 @@ pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, url: JString) {
(on_load.handler)(PageLoadEvent::Finished, url)
}
}
Err(e) => {
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {}", e)
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
172 changes: 172 additions & 0 deletions src/android/kotlin/RequestInterceptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// taken from https://github.com/acsbendi/Android-Request-Inspector-WebView
// Copyright 2022 Bendegúz Ács

@file:Suppress("unused")

package {{package}}

import android.util.Log
import android.webkit.JavascriptInterface
import org.json.JSONArray
import org.json.JSONObject
import java.net.URLEncoder

class RequestInterceptor {
private val interceptedRequests = HashMap<String, RecordedRequest>()

fun removeInterceptedRequest(id: String): RecordedRequest? {
return interceptedRequests.remove(id)
}

data class RecordedRequest(
val url: String,
val body: String,
)

@JavascriptInterface
fun recordFormSubmission(
url: String,
formParameterList: String,
enctype: String?
) {
val formParameterJsonArray = JSONArray(formParameterList)

val body = when (enctype) {
"application/x-www-form-urlencoded" -> {
getUrlEncodedFormBody(formParameterJsonArray)
}

"multipart/form-data" -> {
getMultiPartFormBody(formParameterJsonArray)
}

"text/plain" -> {
getPlainTextFormBody(formParameterJsonArray)
}

else -> {
Log.e("RequestInterceptor", "Incorrect encoding received from JavaScript: $enctype")
""
}
}

addRecordedRequest(
id,
RecordedRequest(
url.removeSuffix("/"),
body
)
)
}

@JavascriptInterface
fun recordXhr(id: String, url: String, body: String) {
addRecordedRequest(
id,
RecordedRequest(
url,
body
)
)
}

@JavascriptInterface
fun recordFetch(id: String, url: String, body: String) {
addRecordedRequest(
id,
RecordedRequest(
url,
body
)
)
}

private fun addRecordedRequest(id: String, recordedRequest: RecordedRequest) {
interceptedRequests[id] = recordedRequest
}

private fun getUrlEncodedFormBody(formParameterJsonArray: JSONArray): String {
val resultStringBuilder = StringBuilder()
repeat(formParameterJsonArray.length()) { i ->
val formParameter = formParameterJsonArray.get(i) as JSONObject
val name = formParameter.getString("name")
val value = formParameter.optString("value")
val checked = formParameter.optBoolean("checked")
val type = formParameter.optString("type")
val encodedValue = URLEncoder.encode(value, "UTF-8")

if (!isExcludedFormParameter(type, checked)) {
if (i != 0) {
resultStringBuilder.append("&")
}
resultStringBuilder.append(name)
resultStringBuilder.append("=")
resultStringBuilder.append(encodedValue)
}


}
return resultStringBuilder.toString()
}

private fun getMultiPartFormBody(formParameterJsonArray: JSONArray): String {
val resultStringBuilder = StringBuilder()
repeat(formParameterJsonArray.length()) { i ->
val formParameter = formParameterJsonArray.get(i) as JSONObject
val name = formParameter.getString("name")
val value = formParameter.optString("value")
val checked = formParameter.optBoolean("checked")
val type = formParameter.optString("type")

if (!isExcludedFormParameter(type, checked)) {
resultStringBuilder.append("--")
resultStringBuilder.append(MULTIPART_FORM_BOUNDARY)
resultStringBuilder.append("\n")
resultStringBuilder.append("Content-Disposition: form-data; name=\"$name\"")
resultStringBuilder.append("\n\n")
resultStringBuilder.append(value)
resultStringBuilder.append("\n")
}

}
resultStringBuilder.append("--")
resultStringBuilder.append(MULTIPART_FORM_BOUNDARY)
resultStringBuilder.append("--")
return resultStringBuilder.toString()
}

private fun getPlainTextFormBody(formParameterJsonArray: JSONArray): String {
val resultStringBuilder = StringBuilder()
repeat(formParameterJsonArray.length()) { i ->
val formParameter = formParameterJsonArray.get(i) as JSONObject
val name = formParameter.getString("name")
val value = formParameter.optString("value")
val checked = formParameter.optBoolean("checked")
val type = formParameter.optString("type")

if (!isExcludedFormParameter(type, checked)) {
if (i != 0) {
resultStringBuilder.append("\n")
}
resultStringBuilder.append(name)
resultStringBuilder.append("=")
resultStringBuilder.append(value)
}

}
return resultStringBuilder.toString()
}

private fun isExcludedFormParameter(type: String, checked: Boolean): Boolean {
return (type == "radio" || type == "checkbox") && !checked
}

companion object {
private const val MULTIPART_FORM_BOUNDARY = "----WebKitFormBoundaryU7CgQs9WnqlZYKs6"
const val INTERFACE_NAME = "RequestInterceptor"
const val REQUEST_ID_HEADER_NAME = "wry-internal-request-id"
}
}
4 changes: 4 additions & 0 deletions src/android/kotlin/RustWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlin.collections.Map
@SuppressLint("RestrictedApi")
class RustWebView(context: Context, val initScripts: Array<String>, val id: String): WebView(context) {
val isDocumentStartScriptEnabled: Boolean
val requestInterceptor: RequestInterceptor

init {
settings.javaScriptEnabled = true
Expand All @@ -34,6 +35,9 @@ class RustWebView(context: Context, val initScripts: Array<String>, val id: Stri
isDocumentStartScriptEnabled = false
}

requestInterceptor = RequestInterceptor()
addJavascriptInterface(requestInterceptor, RequestInterceptor.INTERFACE_NAME)

{{class-init}}
}

Expand Down
9 changes: 6 additions & 3 deletions src/android/kotlin/RustWebViewClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ class RustWebViewClient(context: Context): WebViewClient() {
return if (withAssetLoader()) {
assetLoader.shouldInterceptRequest(request.url)
} else {
val rustWebview = view as RustWebView;
val response = handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled)
val rustWebview = view as RustWebView
val interceptedRequest = rustWebview.requestInterceptor.removeInterceptedRequest(
request.requestHeaders.remove(RequestInterceptor.REQUEST_ID_HEADER_NAME) ?: request.url.toString().removeSuffix("/")
)
val response = handleRequest(rustWebview.id, request, interceptedRequest?.body ?: "", rustWebview.isDocumentStartScriptEnabled)
interceptedState[request.url.toString()] = response != null
return response
}
Expand Down Expand Up @@ -96,7 +99,7 @@ class RustWebViewClient(context: Context): WebViewClient() {

private external fun assetLoaderDomain(): String
private external fun withAssetLoader(): Boolean
private external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse?
private external fun handleRequest(webviewId: String, request: WebResourceRequest, body: String, isDocumentStartScriptEnabled: Boolean): WebResourceResponse?
private external fun shouldOverride(url: String): Boolean
private external fun onPageLoading(url: String)
private external fun onPageLoaded(url: String)
Expand Down
Loading
Loading