From 95d19b0e3a555442537e7b5a0c6d9387d199072d Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Fri, 30 Aug 2019 16:13:08 -0400 Subject: [PATCH 1/2] fix(core): Remove problematic base64 encoding of multipart form pieces --- .../main/default/classes/IBMWatsonClient.cls | 6 +-- .../classes/IBMWatsonMultipartBody.cls | 53 +++++++------------ 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/force-app/main/default/classes/IBMWatsonClient.cls b/force-app/main/default/classes/IBMWatsonClient.cls index 6c1cb0ff..8897b01e 100644 --- a/force-app/main/default/classes/IBMWatsonClient.cls +++ b/force-app/main/default/classes/IBMWatsonClient.cls @@ -32,9 +32,9 @@ public class IBMWatsonClient { if (request.getMethod().equals('POST') || request.getMethod().equals('PUT')) { if (request.getBody() instanceof IBMWatsonMultipartBody) { // form-multipart request will be send as Base64 request - String form64 = ((IBMWatsonMultipartBody)request.getBody()).form64(); - httpRequest.setBodyAsBlob(EncodingUtil.base64Decode(form64)); - httpRequest.setHeader('Content-Length', String.valueof(form64.length())); + String multipartBody = ((IBMWatsonMultipartBody)request.getBody()).multipartBody(); + httpRequest.setBodyAsBlob(Blob.valueOf(multipartBody)); + httpRequest.setHeader('Content-Length', String.valueof(multipartBody.length())); } else if (request.getBody().contentType.toString().contains(IBMWatsonHttpMediaType.APPLICATION_JSON)) { httpRequest.setBody(IBMWatsonJSONUtil.serialize(request.getBody().content)); } else { diff --git a/force-app/main/default/classes/IBMWatsonMultipartBody.cls b/force-app/main/default/classes/IBMWatsonMultipartBody.cls index d35087a5..c0e1cac8 100644 --- a/force-app/main/default/classes/IBMWatsonMultipartBody.cls +++ b/force-app/main/default/classes/IBMWatsonMultipartBody.cls @@ -19,7 +19,7 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody { private IBMWatsonMediaType originalType; private IBMWatsonMediaType contentType; private List parts; - private String form64; + private String multipartBody; private Blob formBlob; private Map headers; private long contentLength = -1L; @@ -27,11 +27,11 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody { IBMWatsonMultipartBody(String boundary, IBMWatsonMediaType mediaType, List parts) { this.boundary = boundary; this.originalType = mediaType; - this.contentType = IBMWatsonMediaType.parse(mediaType + '; boundary=' + EncodingUtil.urlEncode(boundary, 'UTF-8')); + this.contentType = IBMWatsonMediaType.parse(mediaType + '; boundary=' + boundary); this.parts = parts; this.headers = new Map(); this.contentLength = 0; - writeForm64(parts); + writeMultipartBody(parts); } public IBMWatsonMediaType contentType() { @@ -42,17 +42,17 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody { return formBlob; } - public String form64() { - return form64; + public String multipartBody() { + return multipartBody; } public long contentLength() { return contentLength; } - private long writeForm64(List parts) { - headers.put('Content-Type', 'multipart/form-data; boundary="' + boundary + '"'); - form64 = ''; + private long writeMultipartBody(List parts) { + headers.put('Content-Type', 'multipart/form-data; boundary=' + boundary); + multipartBody = ''; for (Integer i = 0; i < parts.size(); i++) { Part p = parts[i]; Boolean isEndingPart = (i == parts.size() - 1); @@ -60,14 +60,14 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody { String fileName = p.body().name; String mimeType = p.body.bodyContentType().toString(); String file64Body = EncodingUtil.base64Encode(p.body().blobContent); - form64 += writeBlobBody(p.headers().get('Content-Disposition'), file64Body, mimeType, isEndingPart); + multipartBody += writeBlobBody(p.headers().get('Content-Disposition'), file64Body, mimeType, isEndingPart); } else { - form64 += writeBoundary(); - form64 += writeBodyParameter(p.headers().get('Content-Disposition'), p.body().content, isEndingPart); + multipartBody += writeBoundary(); + multipartBody += writeBodyParameter(p.headers().get('Content-Disposition'), p.body().content, isEndingPart); } } - return form64.length(); + return multipartBody.length(); } /** @@ -93,23 +93,14 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody { public String writeBodyParameter(String key, String value, Boolean isEndingPart) { String contentDisposition = 'Content-Disposition: ' + key; String contentDispositionCrLf = contentDisposition + CRLF + CRLF; - Blob contentDispositionCrLfBlob = blob.valueOf(contentDispositionCrLf); - String contentDispositionCrLf64 = EncodingUtil.base64Encode(contentDispositionCrLfBlob); - String content = safelyPad(contentDisposition, contentDispositionCrLf64, CRLF + CRLF); + String content = contentDispositionCrLf; + String valueCrLf = value + CRLF; - Blob valueCrLfBlob = blob.valueOf(valueCrLf); - String valueCrLf64 = EncodingUtil.base64Encode(valueCrLfBlob); + content += valueCrLf; - content += safelyPad(value, valueCrLf64, CRLF); if (isEndingPart == true) { String footer = '--' + this.boundary + '--'; - Blob footerBlob = blob.valueOf(footer); - String footerEncoded = EncodingUtil.base64Encode(footerBlob); - while (footerEncoded.endsWith('=')) { - footer += ' '; - footerEncoded = EncodingUtil.base64Encode(blob.valueOf(footer)); - } - content += safelyPad(footer, footerEncoded, CRLF); + content = content + footer; } return content; } @@ -165,15 +156,7 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody { } public String writeBoundary() { - String value = '--' + boundary; - String valueCrlf = value + CRLF; - Blob valueBlob = blob.valueOf(valueCrlf); - String boundaryEncoded = EncodingUtil.base64Encode(valueBlob); - while (boundaryEncoded.endsWith('=')) { - valueCrlf += ' '; - boundaryEncoded = EncodingUtil.base64Encode(blob.valueOf(valueCrlf)); - } - return boundaryEncoded; + return '--' + this.boundary + CRLF; } public Map getAllHeaders() { @@ -257,7 +240,7 @@ public class IBMWatsonMultipartBody extends IBMWatsonRequestBody { private static String generateRandomBoundaryString() { Blob b = Crypto.GenerateAESKey(128); String h = EncodingUtil.ConvertTohex(b); - String boundaryString = h.substring(0, 8); + String boundaryString = h.substring(0, 16); return boundaryString; } From 800cafd698f5bd73e137abfc903498222926c34f Mon Sep 17 00:00:00 2001 From: Logan Patino Date: Fri, 30 Aug 2019 16:13:20 -0400 Subject: [PATCH 2/2] test: Update multipart form test --- force-app/main/default/classes/IBMWatsonServiceTest.cls | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/force-app/main/default/classes/IBMWatsonServiceTest.cls b/force-app/main/default/classes/IBMWatsonServiceTest.cls index de8b0c0d..4fcc913c 100644 --- a/force-app/main/default/classes/IBMWatsonServiceTest.cls +++ b/force-app/main/default/classes/IBMWatsonServiceTest.cls @@ -363,11 +363,10 @@ private class IBMWatsonServiceTest { .addPart(new Map{'test' => 'test', 'Content-Disposition'=>'Content-Disposition'}, IBMWatsonRequestBody.create()) .addFormDataPart('key', 'value') .build(); - System.assertEquals(IBMWatsonMultipartBody.safelyPad('test', 'test=test=', 'test'), EncodingUtil.base64Encode(blob.valueOf('test test'))); - System.assertEquals(multipartBody.writeBodyParameter('test', 'test', false), 'Q29udGVudC1EaXNwb3NpdGlvbjogdGVzdCANCg0KdGVzdA0K'); + System.assertEquals('Content-Disposition: test\r\n\r\ntest\r\n', multipartBody.writeBodyParameter('test', 'test', false)); System.assertEquals(multipartBody.parts().size(), 2); System.assert(multipartBody.getAllHeaders().get('Content-Type').contains('multipart/form-data; boundary')); - System.assert(multipartBody.form64().contains(multipartBody.writeBoundary())); + System.assert(multipartBody.multipartBody().contains(multipartBody.writeBoundary())); Test.stopTest(); }