From b265be8e7472de09979a533d7ecb3e4e6beb8877 Mon Sep 17 00:00:00 2001 From: "Marc R. Hoffmann" Date: Tue, 9 Apr 2024 20:32:33 +0200 Subject: [PATCH] Random multipart boundary (#40) --- .../com/mountainminds/three4j/Gateway.java | 8 +- .../mountainminds/three4j/HttpSupport.java | 77 +++++++++++++------ .../three4j/HttpSupportTest.java | 14 ++-- 3 files changed, 66 insertions(+), 33 deletions(-) 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