From: Kevin Date: Sat, 4 Jun 2022 09:11:35 +0000 (+0200) Subject: Added base64 encoded attachment support (#966) X-Git-Tag: v0.10.8~18 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/cb5e3c6bf748c9d020218033eabdcc3c5680c81a Added base64 encoded attachment support (#966) * Added base64 encoded attachment support * Added final * Added full RFC 2397 support * Added feedback * Update doc * Update signal-cli.1.adoc Co-authored-by: Sebastian Scheibner --- diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index 8793ce93..0d560e14 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -51,7 +51,7 @@ public class AttachmentHelper { } public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException { - var attachmentStream = AttachmentUtils.createAttachmentStream(new File(attachment)); + var attachmentStream = AttachmentUtils.createAttachmentStream(attachment); return uploadAttachment(attachmentStream); } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java index a94d7673..4da78f68 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java @@ -6,6 +6,7 @@ import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -19,17 +20,18 @@ public class AttachmentUtils { } final var signalServiceAttachments = new ArrayList(attachments.size()); for (var attachment : attachments) { - signalServiceAttachments.add(createAttachmentStream(new File(attachment))); + signalServiceAttachments.add(createAttachmentStream(attachment)); } return signalServiceAttachments; } - public static SignalServiceAttachmentStream createAttachmentStream(File attachmentFile) throws AttachmentInvalidException { + public static SignalServiceAttachmentStream createAttachmentStream(String attachment) throws AttachmentInvalidException { try { - final var streamDetails = Utils.createStreamDetailsFromFile(attachmentFile); - return createAttachmentStream(streamDetails, Optional.of(attachmentFile.getName())); + final var streamDetails = Utils.createStreamDetails(attachment); + + return createAttachmentStream(streamDetails.first(), streamDetails.second()); } catch (IOException e) { - throw new AttachmentInvalidException(attachmentFile.toString(), e); + throw new AttachmentInvalidException(attachment, e); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/DataURI.java b/lib/src/main/java/org/asamk/signal/manager/util/DataURI.java new file mode 100644 index 00000000..04d4bbc8 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/util/DataURI.java @@ -0,0 +1,70 @@ +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; + +@SuppressWarnings({"java:S6218"}) +public record DataURI(String mediaType, Map parameter, byte[] data) { + + public static final Pattern DATA_URI_PATTERN = Pattern.compile( + "\\Adata:(?.+?/.+?)?(?;.+?=.+?)?(?;base64)?,(?.+)\\z", + Pattern.CASE_INSENSITIVE); + public static final Pattern PARAMETER_PATTERN = Pattern.compile("\\G;(?.+)=(?.+)", + Pattern.CASE_INSENSITIVE); + public static final String DEFAULT_TYPE = "text/plain"; + + /** + * Generates a new {@link DataURI} object that follows + * RFC 2397 from the given string. + *

+ * The {@code dataURI} must be of the form: + *

+ * {@code + * data:[][;base64], + * } + *

+ * The {@code } is an Internet media type specification (with + * optional parameters.) The appearance of ";base64" means that the data + * is encoded as base64. Without ";base64", the data is represented using (ASCII) URL Escaped encoding. + * If {@code } is omitted, it defaults to {@link DataURI#DEFAULT_TYPE}. + * Parameter values should use the URL Escaped encoding. + * + * @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 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); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index 3fc69801..e71e6412 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager.util; +import org.asamk.signal.manager.api.Pair; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.fingerprint.Fingerprint; import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator; @@ -9,6 +10,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.StreamDetails; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -17,9 +19,11 @@ import java.net.URLConnection; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.Base64; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Spliterator; import java.util.Spliterators; import java.util.function.BiFunction; @@ -31,10 +35,10 @@ public class Utils { 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()); if (mime == null) { - try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { + try (final InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { mime = URLConnection.guessContentTypeFromStream(bufferedStream); } } @@ -44,13 +48,31 @@ public class Utils { return mime; } - public static StreamDetails createStreamDetailsFromFile(File file) throws IOException { - InputStream stream = new FileInputStream(file); + public static Pair> createStreamDetailsFromDataURI(final String dataURI) { + final DataURI uri = DataURI.of(dataURI); + + return new Pair<>(new StreamDetails( + new ByteArrayInputStream(uri.data()), uri.mediaType(), uri.data().length), + Optional.ofNullable(uri.parameter().get("filename"))); + } + + public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException { + final InputStream stream = new FileInputStream(file); final var size = file.length(); final var mime = getFileMimeType(file, "application/octet-stream"); return new StreamDetails(stream, mime, size); } + public static Pair> createStreamDetails(final String value) throws IOException { + try { + return createStreamDetailsFromDataURI(value); + } catch (final IllegalArgumentException e) { + final File f = new File(value); + + return new Pair<>(createStreamDetailsFromFile(f), Optional.of(f.getName())); + } + } + public static Fingerprint computeSafetyNumber( boolean isUuidCapable, SignalServiceAddress ownAddress, diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 6258243a..d60ef3ad 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -231,6 +231,9 @@ Read the message from standard input. *-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]:: Add one or more files as attachment. +Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. +Additionally a file name can be added: +e.g.: `data:;filename=;base64,` *--sticker* STICKER:: Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId). diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index ae324c84..411895dd 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -55,7 +55,9 @@ public class SendCommand implements JsonRpcLocalCommand { mut.addArgument("--message-from-stdin") .action(Arguments.storeTrue()) .help("Read the message from standard input."); - subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment"); + subparser.addArgument("-a", "--attachment").nargs("*").help("Add an attachment. " + + "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. " + + "data:;filename=;base64,."); subparser.addArgument("-e", "--end-session", "--endsession") .help("Clear session state and send end session message.") .action(Arguments.storeTrue());