From 58045b06ae3921f4204d09ad4ce921a24a46b5c1 Mon Sep 17 00:00:00 2001 From: Michael Wolz Date: Tue, 18 Jun 2024 16:33:42 +0200 Subject: [PATCH 1/3] fix(ios): fix boundary value extraction for form-data requests The changes introduced in #7306 fixed several bugs with multipart/form-data requests on iOS. However, the extraction of the actual boundary value might fail due to the value being surrounded by double quotes, which is allowed and happens occasionally. Additionally, the `Content-Type` header might include other keys such as `charset`. This change extracts the boundary value regardless of the order of the individual keys within the `Content-Type` header. --- .../Plugins/CapacitorUrlRequest.swift | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift b/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift index d13457798..19750a253 100644 --- a/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift +++ b/ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift @@ -63,7 +63,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate { var data = Data() var boundary = UUID().uuidString - if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last { + if contentType.contains("boundary="), let contentBoundary = extractBoundary(from: contentType) { boundary = contentBoundary } else { overrideContentType(boundary) @@ -83,6 +83,27 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate { request.setValue(contentType, forHTTPHeaderField: "Content-Type") headers["Content-Type"] = contentType } + + /** + Extracts the boundary value of the `content-type` header for multiplart/form-data requests, if provided + The boundary value might be surrounded by double quotes (") which will be stripped away. + */ + private func extractBoundary(from contentType: String) -> String? { + if let boundaryRange = contentType.range(of: "boundary=") { + var boundary = contentType[boundaryRange.upperBound...] + if let endRange = boundary.range(of: ";") { + boundary = boundary[.. Data { guard let stringData = data as? String else { @@ -107,7 +128,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate { } var data = Data() var boundary = UUID().uuidString - if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last { + if contentType.contains("boundary="), let contentBoundary = extractBoundary(from: contentType) { boundary = contentBoundary } else { overrideContentType(boundary) From bc6a6e0e70f2e756396099489135913c68996ebc Mon Sep 17 00:00:00 2001 From: Michael Wolz Date: Wed, 19 Jun 2024 08:51:37 +0200 Subject: [PATCH 2/3] fix(android): fix boundary value extraction for form-data requests Applies changes made to iOS in 58045b0 to android platform --- .../util/CapacitorHttpUrlConnection.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java index 6f04cc30e..9a0782089 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java @@ -262,7 +262,7 @@ private void writeObjectRequestBody(JSObject object) throws IOException, JSONExc private void writeFormDataRequestBody(String contentType, JSArray entries) throws IOException, JSONException { try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { - String boundary = contentType.split(";")[1].split("=")[1]; + String boundary = extractBoundary(contentType); String lineEnd = "\r\n"; String twoHyphens = "--"; @@ -301,6 +301,39 @@ private void writeFormDataRequestBody(String contentType, JSArray entries) throw } } + /** + * Extracts the boundary value from the `Content-Type` header for multipart/form-data requests, if provided. + * + * The boundary value might be surrounded by double quotes (") which will be stripped away. + * + * @param contentType The `Content-Type` header string. + * @return The boundary value if found, otherwise `null`. + */ + public static String extractBoundary(String contentType) { + String boundaryPrefix = "boundary="; + int boundaryIndex = contentType.indexOf(boundaryPrefix); + if (boundaryIndex == -1) { + return null; + } + + // Extract the substring starting right after "boundary=" + String boundary = contentType.substring(boundaryIndex + boundaryPrefix.length()); + + // Find the end of the boundary value by looking for the next ";" + int endIndex = boundary.indexOf(";"); + if (endIndex != -1) { + boundary = boundary.substring(0, endIndex); + } + + // Remove surrounding double quotes if present + boundary = boundary.trim(); + if (boundary.startsWith("\"") && boundary.endsWith("\"")) { + boundary = boundary.substring(1, boundary.length() - 1); + } + + return boundary; + } + /** * Opens a communications link to the resource referenced by this * URL, if such a connection has not already been established. From 459e60c62314796168f98cf942ee19b713aa939e Mon Sep 17 00:00:00 2001 From: Michael Wolz Date: Wed, 19 Jun 2024 13:52:27 +0200 Subject: [PATCH 3/3] fix(android): add fallback for form-data boundary --- .../plugin/util/CapacitorHttpUrlConnection.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java index 9a0782089..70e2e7b69 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.UUID; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import org.json.JSONException; @@ -224,7 +225,16 @@ public void setRequestBody(PluginCall call, JSValue body, String bodyType) throw this.writeRequestBody(body.toString()); } } else if (bodyType != null && bodyType.equals("formData")) { - this.writeFormDataRequestBody(contentType, body.toJSArray()); + String boundary = extractBoundaryFromContentType(contentType); + if (boundary == null) { + // If no boundary is provided, generate a random one and set the Content-Type header accordingly + // or otherwise servers will not be able to parse the request body. Browsers do this automatically + // but here we need to do this manually in order to comply with browser api behavior. + boundary = UUID.randomUUID().toString(); + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + } + + this.writeFormDataRequestBody(boundary, body.toJSArray()); } else { this.writeRequestBody(body.toString()); } @@ -260,9 +270,8 @@ private void writeObjectRequestBody(JSObject object) throws IOException, JSONExc } } - private void writeFormDataRequestBody(String contentType, JSArray entries) throws IOException, JSONException { + private void writeFormDataRequestBody(String boundary, JSArray entries) throws IOException, JSONException { try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { - String boundary = extractBoundary(contentType); String lineEnd = "\r\n"; String twoHyphens = "--"; @@ -309,7 +318,7 @@ private void writeFormDataRequestBody(String contentType, JSArray entries) throw * @param contentType The `Content-Type` header string. * @return The boundary value if found, otherwise `null`. */ - public static String extractBoundary(String contentType) { + public static String extractBoundaryFromContentType(String contentType) { String boundaryPrefix = "boundary="; int boundaryIndex = contentType.indexOf(boundaryPrefix); if (boundaryIndex == -1) {