]> nmode's Git Repositories - signal-cli/commitdiff
Added base64 encoded attachment support (#966)
authorKevin <dev@roebert.eu>
Sat, 4 Jun 2022 09:11:35 +0000 (11:11 +0200)
committerGitHub <noreply@github.com>
Sat, 4 Jun 2022 09:11:35 +0000 (11:11 +0200)
* 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 <asamk@gmx.de>
lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java
lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java
lib/src/main/java/org/asamk/signal/manager/util/DataURI.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/util/Utils.java
man/signal-cli.1.adoc
src/main/java/org/asamk/signal/commands/SendCommand.java

index 8793ce936e64459392cb957d815c3c67c8d1d8f0..0d560e14e077763b8388c728b1d7fbefe24bd35c 100644 (file)
@@ -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);
     }
 
index a94d76738f442040abf55b83158ad253e1c4d575..4da78f68696ed8840ab40ef35412e7827777b8e4 100644 (file)
@@ -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<SignalServiceAttachmentStream>(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 (file)
index 0000000..04d4bbc
--- /dev/null
@@ -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<String, String> parameter, byte[] data) {
+
+    public static final Pattern DATA_URI_PATTERN = Pattern.compile(
+            "\\Adata:(?<type>.+?/.+?)?(?<parameters>;.+?=.+?)?(?<base64>;base64)?,(?<data>.+)\\z",
+            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 that follows
+     * <a href="https://datatracker.ietf.org/doc/html/rfc2397">RFC 2397</a> from the given string.
+     * <p>
+     * The {@code dataURI} must be of the form:
+     * <p>
+     * {@code
+     * data:[<mediatype>][;base64],<data>
+     * }
+     * <p>
+     * The {@code <mediatype>} 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 <mediatype>} 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<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);
+    }
+}
index 3fc69801001d3f6e0d65b3c2999d2e5d55be4d24..e71e6412bfe91b5a97ee2631fb36e6465fca4019 100644 (file)
@@ -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<StreamDetails, Optional<String>> 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<StreamDetails, Optional<String>> 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,
index 6258243a5a6387177216556775f8e065a0e6b8db..d60ef3ad275fcdccdc5358da942e295cc34f7c57 100644 (file)
@@ -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:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
 
 *--sticker* STICKER::
 Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
index ae324c84c89dfb11e94840df486aa632515c0cdb..411895dd29f19062f10db70fa9c14e720a6de6c1 100644 (file)
@@ -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:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>.");
         subparser.addArgument("-e", "--end-session", "--endsession")
                 .help("Clear session state and send end session message.")
                 .action(Arguments.storeTrue());