diff --git a/src/main/java/com/mountainminds/three4j/Gateway.java b/src/main/java/com/mountainminds/three4j/Gateway.java
index 198bb38..3dfc43c 100644
--- a/src/main/java/com/mountainminds/three4j/Gateway.java
+++ b/src/main/java/com/mountainminds/three4j/Gateway.java
@@ -20,9 +20,7 @@
import static com.mountainminds.three4j.GatewayException.STATUS_PAYLOADTOOLARGE;
import static com.mountainminds.three4j.GatewayException.STATUS_PAYMENTREQUIRED;
import static com.mountainminds.three4j.GatewayException.STATUS_UNAUTHORIZED;
-import static com.mountainminds.three4j.HttpSupport.MULTIPART_BOUNDARY;
import static com.mountainminds.three4j.HttpSupport.UNKNOWN_RESPONSE;
-import static com.mountainminds.three4j.HttpSupport.blobBody;
import static java.util.Arrays.stream;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
@@ -41,6 +39,7 @@
import java.util.Map;
import java.util.Set;
+import com.mountainminds.three4j.HttpSupport.MultipartEncoder;
import com.mountainminds.three4j.HttpSupport.StatusHandler;
/**
@@ -361,9 +360,10 @@ public MessageId sendMessage(ThreemaId toThreemid, EncryptedMessage msg) throws
* @throws IOException when a technical communication problem occurs
*/
public BlobId uploadBlob(byte[] encryptedcontent) throws GatewayException, IOException {
+ var encoder = new MultipartEncoder(encryptedcontent);
var request = gwRequest(auth(), "upload_blob") //
- .header("Content-Type", "multipart/form-data;boundary=" + MULTIPART_BOUNDARY) //
- .POST(BodyPublishers.ofByteArray(blobBody(encryptedcontent))).build();
+ .header("Content-Type", encoder.getContentType()) //
+ .POST(BodyPublishers.ofByteArray(encoder.getBody())).build();
return BlobId.of(send(request, BodyHandlers.ofString(), DEFAULT_STATUS //
.error(STATUS_BADREQUEST, "required parameters missing or blob empty") //
.error(STATUS_PAYLOADTOOLARGE, "blob is too big")));
diff --git a/src/main/java/com/mountainminds/three4j/HttpSupport.java b/src/main/java/com/mountainminds/three4j/HttpSupport.java
index a13c383..f9526a8 100644
--- a/src/main/java/com/mountainminds/three4j/HttpSupport.java
+++ b/src/main/java/com/mountainminds/three4j/HttpSupport.java
@@ -18,14 +18,15 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.PrintWriter;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
+import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -67,33 +68,63 @@ default StatusHandler ok() {
}
}
- static final String MULTIPART_BOUNDARY = "xZK2aOVCeCybl1bbgvCEas6n4cdntpzkpcLWA12SahAiBrDrkIBj3W2HMPghi3Bo";
-
/**
* Encode binary content as multipart/form-data body according to
* RFC 2046.
*/
- static byte[] blobBody(byte[] blob) {
- try (var out = new ByteArrayOutputStream(); var printer = new PrintWriter(out, true, US_ASCII) {
- @Override
- public void println() {
- // Ensure CRLF line endings on every platform
- write('\r');
- write('\n');
- flush();
- };
- }) {
- printer.println("--" + MULTIPART_BOUNDARY);
- printer.println("Content-Disposition: form-data;name=\"blob\";filename=\"blob\"");
- printer.println();
- out.write(blob);
- printer.println();
- printer.println("--" + MULTIPART_BOUNDARY + "--");
- return out.toByteArray();
- } catch (IOException e) {
- // Must not happen with ByteArrayOutputStream
- throw new RuntimeException(e);
+ static class MultipartEncoder {
+
+ private static ThreadLocal RANDOM = ThreadLocal.withInitial(SecureRandom::new);
+
+ private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ private static final int LEN = 28;
+
+ private final byte[] content;
+ private final String boundary;
+
+ MultipartEncoder(byte[] content) {
+ this(content, RANDOM.get());
+ }
+
+ MultipartEncoder(byte[] content, Random rand) {
+ this.content = content;
+ this.boundary = createBoundary(rand);
}
+
+ private static String createBoundary(Random rand) {
+ return new String(rand.ints(LEN, 0, CHARS.length()).map(CHARS::charAt).toArray(), 0, LEN);
+ }
+
+ String getContentType() {
+ return "multipart/form-data;boundary=" + boundary;
+ }
+
+ byte[] getBody() {
+ try (var out = new ByteArrayOutputStream() {
+ public void println() throws IOException {
+ write('\r');
+ write('\n');
+ };
+
+ public void println(String text) throws IOException {
+ write(text.getBytes(US_ASCII));
+ println();
+ };
+ }) {
+ out.println("--" + boundary);
+ out.println("Content-Disposition: form-data;name=\"blob\";filename=\"blob\"");
+ out.println();
+ out.write(content);
+ out.println();
+ out.println("--" + boundary + "--");
+ return out.toByteArray();
+ } catch (IOException e) {
+ // Must not happen with ByteArrayOutputStream
+ throw new RuntimeException(e);
+ }
+ }
+
}
/**
diff --git a/src/test/java/com/mountainminds/three4j/HttpSupportTest.java b/src/test/java/com/mountainminds/three4j/HttpSupportTest.java
index 02adad0..b7d7210 100644
--- a/src/test/java/com/mountainminds/three4j/HttpSupportTest.java
+++ b/src/test/java/com/mountainminds/three4j/HttpSupportTest.java
@@ -14,7 +14,6 @@
package com.mountainminds.three4j;
import static com.mountainminds.three4j.HttpSupport.UNKNOWN_RESPONSE;
-import static com.mountainminds.three4j.HttpSupport.blobBody;
import static com.mountainminds.three4j.HttpSupport.decodeUrlParams;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -22,9 +21,11 @@
import java.io.IOException;
import java.util.Map;
+import java.util.Random;
import org.junit.jupiter.api.Test;
+import com.mountainminds.three4j.HttpSupport.MultipartEncoder;
import com.mountainminds.three4j.HttpSupport.UrlParams;
public class HttpSupportTest {
@@ -61,13 +62,14 @@ public void StatusHandler_error_should_not_handle_other_codes() {
}
@Test
- public void blobBody_should_create_correct_multipart_content() throws IOException {
- var body = new String(blobBody("".getBytes(US_ASCII)), US_ASCII);
- assertEquals("--xZK2aOVCeCybl1bbgvCEas6n4cdntpzkpcLWA12SahAiBrDrkIBj3W2HMPghi3Bo\r\n" //
+ public void multipartEncoder_should_create_correct_multipart_content() throws IOException {
+ var encoder = new MultipartEncoder("hello".getBytes(US_ASCII), new Random(0));
+ assertEquals("multipart/form-data;boundary=22Pbd7157KhLr8Ry8RZmz66hYkdm", encoder.getContentType());
+ assertEquals("--22Pbd7157KhLr8Ry8RZmz66hYkdm\r\n" //
+ "Content-Disposition: form-data;name=\"blob\";filename=\"blob\"\r\n" //
+ "\r\n" //
- + "\r\n" //
- + "--xZK2aOVCeCybl1bbgvCEas6n4cdntpzkpcLWA12SahAiBrDrkIBj3W2HMPghi3Bo--\r\n", body);
+ + "hello\r\n" //
+ + "--22Pbd7157KhLr8Ry8RZmz66hYkdm--\r\n", new String(encoder.getBody(), US_ASCII));
}
@Test