Added full RFC 2397 support

This commit is contained in:
Kevin R 2022-06-03 15:01:39 +02:00
parent a341bfbe25
commit 57d57508fb
No known key found for this signature in database
GPG key ID: A4AD5E0732960C98
4 changed files with 86 additions and 36 deletions

View file

@ -28,11 +28,8 @@ public class AttachmentUtils {
public static SignalServiceAttachmentStream createAttachmentStream(String attachment) throws AttachmentInvalidException { public static SignalServiceAttachmentStream createAttachmentStream(String attachment) throws AttachmentInvalidException {
try { try {
final var streamDetails = Utils.createStreamDetails(attachment); final var streamDetails = Utils.createStreamDetails(attachment);
final var name = streamDetails.getStream() instanceof FileInputStream
? new File(attachment).getName()
: null;
return createAttachmentStream(streamDetails, Optional.ofNullable(name)); return createAttachmentStream(streamDetails.first(), streamDetails.second());
} catch (IOException e) { } catch (IOException e) {
throw new AttachmentInvalidException(attachment, e); throw new AttachmentInvalidException(attachment, e);
} }

View file

@ -0,0 +1,64 @@
package org.asamk.signal.manager.util;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Data URI record that follows RFC 2397.
*
* @param mediaType the media type. If empty, the default media type "text/plain" is used.
* @param parameter the list of parameters. Must be URL escaped encoded.
* @param data the data. If base64 is not given, the data is treated as ASCII string.
*/
@SuppressWarnings({"java:S6218"})
public record DataURI(String mediaType, Map<String, String> parameter, byte[] data) {
public static final Pattern DATA_URI_PATTERN = Pattern.compile(
"data:(?<type>.+?\\/.+?)?(?<parameters>;.+?)?(?<base64>;base64)?,(?<data>.+)",
Pattern.CASE_INSENSITIVE);
public static final Pattern PARAMETER_PATTERN = Pattern.compile("\\G;(?<key>.+)=(?<value>.+)",
Pattern.CASE_INSENSITIVE);
public static final String DEFAULT_TYPE = "text/plain";
/**
* Generates a new {@link DataURI} object from the given string.
*
* @param dataURI the data URI
* @return a data URI object
* @throws IllegalArgumentException if the given string is not a valid data URI
*/
public static DataURI of(final String dataURI) {
final var matcher = DATA_URI_PATTERN.matcher(dataURI);
if (!matcher.find()) {
throw new IllegalArgumentException("The given string is not a valid data URI.");
}
final Map<String, String> parameters = new HashMap<>();
final var params = matcher.group("parameters");
if (params != null) {
final Matcher paramsMatcher = PARAMETER_PATTERN.matcher(params);
while (paramsMatcher.find()) {
final var key = paramsMatcher.group("key");
final var value = URLDecoder.decode(paramsMatcher.group("value"), StandardCharsets.UTF_8);
parameters.put(key, value);
}
}
final boolean isBase64 = matcher.group("base64") != null;
final byte[] data;
if (isBase64) {
data = Base64.getDecoder().decode(matcher.group("data").getBytes(StandardCharsets.UTF_8));
} else {
data = URLDecoder.decode(matcher.group("data"), StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
}
return new DataURI(Optional.ofNullable(matcher.group("type")).orElse(DEFAULT_TYPE), parameters, data);
}
}

View file

@ -1,5 +1,6 @@
package org.asamk.signal.manager.util; package org.asamk.signal.manager.util;
import org.asamk.signal.manager.api.Pair;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.fingerprint.Fingerprint; import org.signal.libsignal.protocol.fingerprint.Fingerprint;
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator; import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
@ -22,6 +23,7 @@ import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Spliterator; import java.util.Spliterator;
import java.util.Spliterators; import java.util.Spliterators;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@ -33,10 +35,10 @@ public class Utils {
private final static Logger logger = LoggerFactory.getLogger(Utils.class); private final static Logger logger = LoggerFactory.getLogger(Utils.class);
public static String getFileMimeType(File file, String defaultMimeType) throws IOException { public static String getFileMimeType(final File file, final String defaultMimeType) throws IOException {
var mime = Files.probeContentType(file.toPath()); var mime = Files.probeContentType(file.toPath());
if (mime == null) { if (mime == null) {
try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { try (final InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
mime = URLConnection.guessContentTypeFromStream(bufferedStream); mime = URLConnection.guessContentTypeFromStream(bufferedStream);
} }
} }
@ -46,42 +48,29 @@ public class Utils {
return mime; return mime;
} }
private static boolean isBase64DataString(final String[] parts) { public static Pair<StreamDetails, Optional<String>> createStreamDetailsFromDataURI(final String dataURI) {
return parts.length == 2 final DataURI uri = DataURI.of(dataURI);
&& parts[0].startsWith("data:")
&& parts[0].contains("/") return new Pair<>(new StreamDetails(
&& parts[1].startsWith("base64,"); new ByteArrayInputStream(uri.data()), uri.mediaType(), uri.data().length),
Optional.ofNullable(uri.parameter().get("filename")));
} }
public static boolean isBase64DataString(final String value) { public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException {
return isBase64DataString(value.split(";", 2)); final InputStream stream = new FileInputStream(file);
}
public static StreamDetails createStreamDetailsFromBase64(final String base64) {
final String[] parts = base64.split(";", 2);
if (!isBase64DataString(parts)) {
throw new IllegalArgumentException("The given argument is not a valid base64 string.");
}
parts[0] = parts[0].substring(5);
final byte[] bytes = Base64.getDecoder().decode(parts[1].substring(7).getBytes(StandardCharsets.UTF_8));
return new StreamDetails(new ByteArrayInputStream(bytes), parts[0], bytes.length);
}
public static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
InputStream stream = new FileInputStream(file);
final var size = file.length(); final var size = file.length();
final var mime = getFileMimeType(file, "application/octet-stream"); final var mime = getFileMimeType(file, "application/octet-stream");
return new StreamDetails(stream, mime, size); return new StreamDetails(stream, mime, size);
} }
public static StreamDetails createStreamDetails(final String value) throws IOException { public static Pair<StreamDetails, Optional<String>> createStreamDetails(final String value) throws IOException {
if (isBase64DataString(value)) { try {
return createStreamDetailsFromBase64(value); return createStreamDetailsFromDataURI(value);
} } catch (final IllegalArgumentException e) {
final File f = new File(value);
return createStreamDetailsFromFile(new File(value)); return new Pair<>(createStreamDetailsFromFile(f), Optional.of(f.getName()));
}
} }
public static Fingerprint computeSafetyNumber( public static Fingerprint computeSafetyNumber(

View file

@ -56,8 +56,8 @@ public class SendCommand implements JsonRpcLocalCommand {
.action(Arguments.storeTrue()) .action(Arguments.storeTrue())
.help("Read the message from standard input."); .help("Read the message from standard input.");
subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment." subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment."
+ "Base64 encoded attachments can be added and must follow the format " + "Data URI encoded attachments can be added and must follow the RFC 2397. Additionally a file name can be added, e.g. "
+ "data:<MIME-TYPE>;base64,<BASE64 ENCODED DATA>."); + "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>.");
subparser.addArgument("-e", "--end-session", "--endsession") subparser.addArgument("-e", "--end-session", "--endsession")
.help("Clear session state and send end session message.") .help("Clear session state and send end session message.")
.action(Arguments.storeTrue()); .action(Arguments.storeTrue());