Skip to content

Commit

Permalink
Random multipart boundary (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
marchof authored Apr 9, 2024
1 parent b87051d commit b265be8
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 33 deletions.
8 changes: 4 additions & 4 deletions src/main/java/com/mountainminds/three4j/Gateway.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,6 +39,7 @@
import java.util.Map;
import java.util.Set;

import com.mountainminds.three4j.HttpSupport.MultipartEncoder;
import com.mountainminds.three4j.HttpSupport.StatusHandler;

/**
Expand Down Expand Up @@ -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")));
Expand Down
77 changes: 54 additions & 23 deletions src/main/java/com/mountainminds/three4j/HttpSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -67,33 +68,63 @@ default StatusHandler ok() {
}
}

static final String MULTIPART_BOUNDARY = "xZK2aOVCeCybl1bbgvCEas6n4cdntpzkpcLWA12SahAiBrDrkIBj3W2HMPghi3Bo";

/**
* Encode binary content as multipart/form-data body according to
* <a href="https://tools.ietf.org/html/rfc2046">RFC 2046</a>.
*/
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> 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);
}
}

}

/**
Expand Down
14 changes: 8 additions & 6 deletions src/test/java/com/mountainminds/three4j/HttpSupportTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@
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;
import static org.junit.jupiter.api.Assertions.assertThrows;

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 {
Expand Down Expand Up @@ -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("<content>".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" //
+ "<content>\r\n" //
+ "--xZK2aOVCeCybl1bbgvCEas6n4cdntpzkpcLWA12SahAiBrDrkIBj3W2HMPghi3Bo--\r\n", body);
+ "hello\r\n" //
+ "--22Pbd7157KhLr8Ry8RZmz66hYkdm--\r\n", new String(encoder.getBody(), US_ASCII));
}

@Test
Expand Down

0 comments on commit b265be8

Please sign in to comment.