mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
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 <asamk@gmx.de>
This commit is contained in:
parent
63e94a9fb4
commit
cb5e3c6bf7
6 changed files with 110 additions and 11 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
70
lib/src/main/java/org/asamk/signal/manager/util/DataURI.java
Normal file
70
lib/src/main/java/org/asamk/signal/manager/util/DataURI.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue