mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-15 19:33:49 +00:00
Add initial support for send/receive on CDN2.
This commit is contained in:
committed by
Greyson Parrelli
parent
1290d0ead9
commit
37a35e8f70
@@ -26,7 +26,8 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
|
||||
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
@@ -219,7 +220,7 @@ public class SignalServiceMessagePipe {
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentUploadAttributes getAttachmentUploadAttributes() throws IOException {
|
||||
public AttachmentV2UploadAttributes getAttachmentV2UploadAttributes() throws IOException {
|
||||
try {
|
||||
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||
.setId(new SecureRandom().nextLong())
|
||||
@@ -233,7 +234,27 @@ public class SignalServiceMessagePipe {
|
||||
throw new IOException("Non-successful response: " + response.first());
|
||||
}
|
||||
|
||||
return JsonUtil.fromJson(response.second(), AttachmentUploadAttributes.class);
|
||||
return JsonUtil.fromJson(response.second(), AttachmentV2UploadAttributes.class);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentV3UploadAttributes getAttachmentV3UploadAttributes() throws IOException {
|
||||
try {
|
||||
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||
.setId(new SecureRandom().nextLong())
|
||||
.setVerb("GET")
|
||||
.setPath("/v3/attachments/form/upload")
|
||||
.build();
|
||||
|
||||
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
|
||||
|
||||
if (response.first() < 200 || response.first() >= 300) {
|
||||
throw new IOException("Non-successful response: " + response.first());
|
||||
}
|
||||
|
||||
return JsonUtil.fromJson(response.second(), AttachmentV3UploadAttributes.class);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ public class SignalServiceMessageReceiver {
|
||||
{
|
||||
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
|
||||
|
||||
socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener);
|
||||
socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
|
||||
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@@ -50,12 +51,14 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
|
||||
@@ -110,6 +113,7 @@ public class SignalServiceMessageSender {
|
||||
private final AtomicReference<Optional<SignalServiceMessagePipe>> pipe;
|
||||
private final AtomicReference<Optional<SignalServiceMessagePipe>> unidentifiedPipe;
|
||||
private final AtomicBoolean isMultiDevice;
|
||||
private final AtomicBoolean attachmentsV3;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceMessageSender.
|
||||
@@ -127,12 +131,13 @@ public class SignalServiceMessageSender {
|
||||
SignalProtocolStore store,
|
||||
String signalAgent,
|
||||
boolean isMultiDevice,
|
||||
boolean attachmentsV3,
|
||||
Optional<SignalServiceMessagePipe> pipe,
|
||||
Optional<SignalServiceMessagePipe> unidentifiedPipe,
|
||||
Optional<EventListener> eventListener,
|
||||
ClientZkProfileOperations clientZkProfileOperations)
|
||||
{
|
||||
this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations);
|
||||
this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, attachmentsV3, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations);
|
||||
}
|
||||
|
||||
public SignalServiceMessageSender(SignalServiceConfiguration urls,
|
||||
@@ -140,6 +145,7 @@ public class SignalServiceMessageSender {
|
||||
SignalProtocolStore store,
|
||||
String signalAgent,
|
||||
boolean isMultiDevice,
|
||||
boolean attachmentsV3,
|
||||
Optional<SignalServiceMessagePipe> pipe,
|
||||
Optional<SignalServiceMessagePipe> unidentifiedPipe,
|
||||
Optional<EventListener> eventListener,
|
||||
@@ -151,6 +157,7 @@ public class SignalServiceMessageSender {
|
||||
this.pipe = new AtomicReference<>(pipe);
|
||||
this.unidentifiedPipe = new AtomicReference<>(unidentifiedPipe);
|
||||
this.isMultiDevice = new AtomicBoolean(isMultiDevice);
|
||||
this.attachmentsV3 = new AtomicBoolean(attachmentsV3);
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
@@ -336,13 +343,11 @@ public class SignalServiceMessageSender {
|
||||
socket.cancelInFlightRequests();
|
||||
}
|
||||
|
||||
public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) {
|
||||
public void update(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe, boolean isMultiDevice, boolean attachmentsV3) {
|
||||
this.pipe.set(Optional.fromNullable(pipe));
|
||||
this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe));
|
||||
}
|
||||
|
||||
public void setIsMultiDevice(boolean isMultiDevice) {
|
||||
this.isMultiDevice.set(isMultiDevice);
|
||||
this.attachmentsV3.set(attachmentsV3);
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment) throws IOException {
|
||||
@@ -357,26 +362,35 @@ public class SignalServiceMessageSender {
|
||||
attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
|
||||
AttachmentUploadAttributes uploadAttributes = null;
|
||||
Optional<SignalServiceMessagePipe> localPipe = pipe.get();
|
||||
if (attachmentsV3.get()) {
|
||||
return uploadAttachmentV3(attachment, attachmentKey, attachmentData);
|
||||
} else {
|
||||
return uploadAttachmentV2(attachment, attachmentKey, attachmentData);
|
||||
}
|
||||
}
|
||||
|
||||
private SignalServiceAttachmentPointer uploadAttachmentV2(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
AttachmentV2UploadAttributes v2UploadAttributes = null;
|
||||
Optional<SignalServiceMessagePipe> localPipe = pipe.get();
|
||||
|
||||
if (localPipe.isPresent()) {
|
||||
Log.d(TAG, "Using pipe to retrieve attachment upload attributes...");
|
||||
try {
|
||||
uploadAttributes = localPipe.get().getAttachmentUploadAttributes();
|
||||
v2UploadAttributes = localPipe.get().getAttachmentV2UploadAttributes();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to retrieve attachment upload attributes using pipe. Falling back...");
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadAttributes == null) {
|
||||
if (v2UploadAttributes == null) {
|
||||
Log.d(TAG, "Not using pipe to retrieve attachment upload attributes...");
|
||||
uploadAttributes = socket.getAttachmentUploadAttributes();
|
||||
v2UploadAttributes = socket.getAttachmentV2UploadAttributes();
|
||||
}
|
||||
|
||||
Pair<Long, byte[]> attachmentIdAndDigest = socket.uploadAttachment(attachmentData, uploadAttributes);
|
||||
Pair<Long, byte[]> attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes);
|
||||
|
||||
return new SignalServiceAttachmentPointer(attachmentIdAndDigest.first(),
|
||||
return new SignalServiceAttachmentPointer(0,
|
||||
new SignalServiceAttachmentRemoteId(attachmentIdAndDigest.first()),
|
||||
attachment.getContentType(),
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachment.getLength())),
|
||||
@@ -390,6 +404,41 @@ public class SignalServiceMessageSender {
|
||||
attachment.getUploadTimestamp());
|
||||
}
|
||||
|
||||
private SignalServiceAttachmentPointer uploadAttachmentV3(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException {
|
||||
AttachmentV3UploadAttributes v3UploadAttributes = null;
|
||||
Optional<SignalServiceMessagePipe> localPipe = pipe.get();
|
||||
|
||||
if (localPipe.isPresent()) {
|
||||
Log.d(TAG, "Using pipe to retrieve attachment upload attributes...");
|
||||
try {
|
||||
v3UploadAttributes = localPipe.get().getAttachmentV3UploadAttributes();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to retrieve attachment upload attributes using pipe. Falling back...");
|
||||
}
|
||||
}
|
||||
|
||||
if (v3UploadAttributes == null) {
|
||||
Log.d(TAG, "Not using pipe to retrieve attachment upload attributes...");
|
||||
v3UploadAttributes = socket.getAttachmentV3UploadAttributes();
|
||||
}
|
||||
|
||||
byte[] digest = socket.uploadAttachment(attachmentData, v3UploadAttributes);
|
||||
return new SignalServiceAttachmentPointer(v3UploadAttributes.getCdn(),
|
||||
new SignalServiceAttachmentRemoteId(v3UploadAttributes.getKey()),
|
||||
attachment.getContentType(),
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachment.getLength())),
|
||||
attachment.getPreview(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
Optional.of(digest),
|
||||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.getCaption(),
|
||||
attachment.getBlurHash(),
|
||||
attachment.getUploadTimestamp());
|
||||
}
|
||||
|
||||
|
||||
private void sendMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
throws IOException, UntrustedIdentityException
|
||||
@@ -1205,12 +1254,20 @@ public class SignalServiceMessageSender {
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) {
|
||||
AttachmentPointer.Builder builder = AttachmentPointer.newBuilder()
|
||||
.setCdnNumber(attachment.getCdnNumber())
|
||||
.setContentType(attachment.getContentType())
|
||||
.setId(attachment.getId())
|
||||
.setKey(ByteString.copyFrom(attachment.getKey()))
|
||||
.setDigest(ByteString.copyFrom(attachment.getDigest().get()))
|
||||
.setSize(attachment.getSize().get());
|
||||
|
||||
if (attachment.getRemoteId().getV2().isPresent()) {
|
||||
builder.setCdnId(attachment.getRemoteId().getV2().get());
|
||||
}
|
||||
|
||||
if (attachment.getRemoteId().getV3().isPresent()) {
|
||||
builder.setCdnKey(attachment.getRemoteId().getV3().get());
|
||||
}
|
||||
|
||||
if (attachment.getFileName().isPresent()) {
|
||||
builder.setFileName(attachment.getFileName().get());
|
||||
}
|
||||
@@ -1245,8 +1302,7 @@ public class SignalServiceMessageSender {
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment)
|
||||
throws IOException
|
||||
{
|
||||
SignalServiceAttachmentPointer pointer = uploadAttachment(attachment);
|
||||
return createAttachmentPointer(pointer);
|
||||
return createAttachmentPointer(uploadAttachment(attachment));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,28 +18,31 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
*/
|
||||
public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||
|
||||
private final long id;
|
||||
private final byte[] key;
|
||||
private final Optional<Integer> size;
|
||||
private final Optional<byte[]> preview;
|
||||
private final Optional<byte[]> digest;
|
||||
private final Optional<String> fileName;
|
||||
private final boolean voiceNote;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Optional<String> caption;
|
||||
private final Optional<String> blurHash;
|
||||
private final long uploadTimestamp;
|
||||
private final int cdnNumber;
|
||||
private final SignalServiceAttachmentRemoteId remoteId;
|
||||
private final byte[] key;
|
||||
private final Optional<Integer> size;
|
||||
private final Optional<byte[]> preview;
|
||||
private final Optional<byte[]> digest;
|
||||
private final Optional<String> fileName;
|
||||
private final boolean voiceNote;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Optional<String> caption;
|
||||
private final Optional<String> blurHash;
|
||||
private final long uploadTimestamp;
|
||||
|
||||
public SignalServiceAttachmentPointer(long id, String contentType, byte[] key,
|
||||
Optional<Integer> size, Optional<byte[]> preview,
|
||||
int width, int height,
|
||||
Optional<byte[]> digest, Optional<String> fileName,
|
||||
boolean voiceNote, Optional<String> caption,
|
||||
Optional<String> blurHash, long uploadTimestamp)
|
||||
public SignalServiceAttachmentPointer(int cdnNumber, SignalServiceAttachmentRemoteId remoteId,
|
||||
String contentType, byte[] key,
|
||||
Optional<Integer> size, Optional<byte[]> preview, int width,
|
||||
int height, Optional<byte[]> digest,
|
||||
Optional<String> fileName, boolean voiceNote,
|
||||
Optional<String> caption, Optional<String> blurHash,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType);
|
||||
this.id = id;
|
||||
this.cdnNumber = cdnNumber;
|
||||
this.remoteId = remoteId;
|
||||
this.key = key;
|
||||
this.size = size;
|
||||
this.preview = preview;
|
||||
@@ -53,8 +56,12 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||
this.uploadTimestamp = uploadTimestamp;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
public int getCdnNumber() {
|
||||
return cdnNumber;
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentRemoteId getRemoteId() {
|
||||
return remoteId;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
|
||||
/**
|
||||
* Represents a signal service attachment identifier. This can be either a CDN key or a long, but
|
||||
* not both at once. Attachments V2 used a long as an attachment identifier. This lacks sufficient
|
||||
* entropy to reduce the likelihood of any two uploads going to the same location within a 30-day
|
||||
* window. Attachments V3 uses an opaque string as an attachment identifier which provides more
|
||||
* flexibility in the amount of entropy present.
|
||||
*/
|
||||
public final class SignalServiceAttachmentRemoteId {
|
||||
private final Optional<Long> v2;
|
||||
private final Optional<String> v3;
|
||||
|
||||
public SignalServiceAttachmentRemoteId(long v2) {
|
||||
this.v2 = Optional.of(v2);
|
||||
this.v3 = Optional.absent();
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentRemoteId(String v3) {
|
||||
this.v2 = Optional.absent();
|
||||
this.v3 = Optional.of(v3);
|
||||
}
|
||||
|
||||
public Optional<Long> getV2() {
|
||||
return v2;
|
||||
}
|
||||
|
||||
public Optional<String> getV3() {
|
||||
return v3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (v2.isPresent()) {
|
||||
return v2.get().toString();
|
||||
} else {
|
||||
return v3.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static SignalServiceAttachmentRemoteId from(AttachmentPointer attachmentPointer) throws ProtocolInvalidMessageException {
|
||||
switch (attachmentPointer.getAttachmentIdentifierCase()) {
|
||||
case CDNID:
|
||||
return new SignalServiceAttachmentRemoteId(attachmentPointer.getCdnId());
|
||||
case CDNKEY:
|
||||
return new SignalServiceAttachmentRemoteId(attachmentPointer.getCdnKey());
|
||||
case ATTACHMENTIDENTIFIER_NOT_SET:
|
||||
throw new ProtocolInvalidMessageException(new InvalidMessageException("AttachmentPointer CDN location not set"), null, 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guesses that strings which contain values parseable to {@code long} should use an id-based
|
||||
* CDN path. Otherwise, use key-based CDN path.
|
||||
*/
|
||||
public static SignalServiceAttachmentRemoteId from(String string) {
|
||||
try {
|
||||
return new SignalServiceAttachmentRemoteId(Long.parseLong(string));
|
||||
} catch (NumberFormatException e) {
|
||||
return new SignalServiceAttachmentRemoteId(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,7 +562,7 @@ public final class SignalServiceContent {
|
||||
Optional.<byte[]>absent());
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) {
|
||||
private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (!content.hasQuote()) return null;
|
||||
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> attachments = new LinkedList<>();
|
||||
@@ -586,7 +586,7 @@ public final class SignalServiceContent {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SignalServiceDataMessage.Preview> createPreviews(SignalServiceProtos.DataMessage content) {
|
||||
private static List<SignalServiceDataMessage.Preview> createPreviews(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getPreviewCount() <= 0) return null;
|
||||
|
||||
List<SignalServiceDataMessage.Preview> results = new LinkedList<>();
|
||||
@@ -606,7 +606,7 @@ public final class SignalServiceContent {
|
||||
return results;
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) {
|
||||
private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (!content.hasSticker() ||
|
||||
!content.getSticker().hasPackId() ||
|
||||
!content.getSticker().hasPackKey() ||
|
||||
@@ -641,7 +641,7 @@ public final class SignalServiceContent {
|
||||
reaction.getTargetSentTimestamp());
|
||||
}
|
||||
|
||||
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) {
|
||||
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getContactCount() <= 0) return null;
|
||||
|
||||
List<SharedContact> results = new LinkedList<>();
|
||||
@@ -736,8 +736,9 @@ public final class SignalServiceContent {
|
||||
return results;
|
||||
}
|
||||
|
||||
private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) {
|
||||
return new SignalServiceAttachmentPointer(pointer.getId(),
|
||||
private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) throws ProtocolInvalidMessageException {
|
||||
return new SignalServiceAttachmentPointer(pointer.getCdnNumber(),
|
||||
SignalServiceAttachmentRemoteId.from(pointer),
|
||||
pointer.getContentType(),
|
||||
pointer.getKey().toByteArray(),
|
||||
pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.<Integer>absent(),
|
||||
@@ -795,7 +796,8 @@ public final class SignalServiceContent {
|
||||
if (content.getGroup().hasAvatar()) {
|
||||
SignalServiceProtos.AttachmentPointer pointer = content.getGroup().getAvatar();
|
||||
|
||||
avatar = new SignalServiceAttachmentPointer(pointer.getId(),
|
||||
avatar = new SignalServiceAttachmentPointer(pointer.getCdnNumber(),
|
||||
SignalServiceAttachmentRemoteId.from(pointer),
|
||||
pointer.getContentType(),
|
||||
pointer.getKey().toByteArray(),
|
||||
Optional.of(pointer.getSize()),
|
||||
|
||||
@@ -11,6 +11,7 @@ public final class SignalServiceConfiguration {
|
||||
|
||||
private final SignalServiceUrl[] signalServiceUrls;
|
||||
private final SignalCdnUrl[] signalCdnUrls;
|
||||
private final SignalCdnUrl[] signalCdn2Urls;
|
||||
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
|
||||
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
|
||||
private final SignalStorageUrl[] signalStorageUrls;
|
||||
@@ -20,6 +21,7 @@ public final class SignalServiceConfiguration {
|
||||
|
||||
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
|
||||
SignalCdnUrl[] signalCdnUrls,
|
||||
SignalCdnUrl[] signalCdn2Urls,
|
||||
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
|
||||
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
|
||||
SignalStorageUrl[] signalStorageUrls,
|
||||
@@ -29,6 +31,7 @@ public final class SignalServiceConfiguration {
|
||||
{
|
||||
this.signalServiceUrls = signalServiceUrls;
|
||||
this.signalCdnUrls = signalCdnUrls;
|
||||
this.signalCdn2Urls = signalCdn2Urls;
|
||||
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
|
||||
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
|
||||
this.signalStorageUrls = signalStorageUrls;
|
||||
@@ -45,6 +48,10 @@ public final class SignalServiceConfiguration {
|
||||
return signalCdnUrls;
|
||||
}
|
||||
|
||||
public SignalCdnUrl[] getSignalCdn2Urls() {
|
||||
return signalCdn2Urls;
|
||||
}
|
||||
|
||||
public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() {
|
||||
return signalContactDiscoveryUrls;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class AttachmentUploadAttributes {
|
||||
public class AttachmentV2UploadAttributes {
|
||||
@JsonProperty
|
||||
private String url;
|
||||
|
||||
@@ -34,7 +34,7 @@ public class AttachmentUploadAttributes {
|
||||
@JsonProperty
|
||||
private String attachmentIdString;
|
||||
|
||||
public AttachmentUploadAttributes() {}
|
||||
public AttachmentV2UploadAttributes() {}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class AttachmentV3UploadAttributes {
|
||||
@JsonProperty
|
||||
private int cdn;
|
||||
|
||||
@JsonProperty
|
||||
private String key;
|
||||
|
||||
@JsonProperty
|
||||
private Map<String, String> headers;
|
||||
|
||||
@JsonProperty
|
||||
private String signedUploadLocation;
|
||||
|
||||
public AttachmentV3UploadAttributes() {
|
||||
}
|
||||
|
||||
public int getCdn() {
|
||||
return cdn;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getSignedUploadLocation() {
|
||||
return signedUploadLocation;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.whispersystems.signalservice.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
@@ -114,6 +115,7 @@ import okhttp3.Call;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.Dns;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
@@ -159,7 +161,8 @@ public class PushServiceSocket {
|
||||
private static final String MESSAGE_PATH = "/v1/messages/%s";
|
||||
private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d";
|
||||
private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s";
|
||||
private static final String ATTACHMENT_PATH = "/v2/attachments/form/upload";
|
||||
private static final String ATTACHMENT_V2_PATH = "/v2/attachments/form/upload";
|
||||
private static final String ATTACHMENT_V3_PATH = "/v3/attachments/form/upload";
|
||||
|
||||
private static final String PROFILE_PATH = "/v1/profile/%s";
|
||||
private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s";
|
||||
@@ -167,14 +170,15 @@ public class PushServiceSocket {
|
||||
private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery";
|
||||
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true";
|
||||
|
||||
private static final String KBS_AUTH_PATH = "/v1/backup/auth";
|
||||
private static final String KBS_AUTH_PATH = "/v1/backup/auth";
|
||||
|
||||
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
|
||||
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
|
||||
private static final String AVATAR_UPLOAD_PATH = "";
|
||||
private static final String ATTACHMENT_KEY_DOWNLOAD_PATH = "attachments/%s";
|
||||
private static final String ATTACHMENT_ID_DOWNLOAD_PATH = "attachments/%d";
|
||||
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
|
||||
private static final String AVATAR_UPLOAD_PATH = "";
|
||||
|
||||
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
|
||||
private static final String STICKER_PATH = "stickers/%s/full/%d";
|
||||
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
|
||||
private static final String STICKER_PATH = "stickers/%s/full/%d";
|
||||
|
||||
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
|
||||
private static final String GROUPSV2_GROUP = "/v1/groups/";
|
||||
@@ -189,6 +193,7 @@ public class PushServiceSocket {
|
||||
|
||||
private final ServiceConnectionHolder[] serviceClients;
|
||||
private final ConnectionHolder[] cdnClients;
|
||||
private final ConnectionHolder[] cdn2Clients;
|
||||
private final ConnectionHolder[] contactDiscoveryClients;
|
||||
private final ConnectionHolder[] keyBackupServiceClients;
|
||||
private final ConnectionHolder[] storageClients;
|
||||
@@ -207,6 +212,7 @@ public class PushServiceSocket {
|
||||
this.signalAgent = signalAgent;
|
||||
this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.cdnClients = createConnectionHolders(configuration.getSignalCdnUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.cdn2Clients = createConnectionHolders(configuration.getSignalCdn2Urls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.contactDiscoveryClients = createConnectionHolders(configuration.getSignalContactDiscoveryUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
@@ -516,17 +522,23 @@ public class PushServiceSocket {
|
||||
makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity));
|
||||
}
|
||||
|
||||
public void retrieveAttachment(long attachmentId, File destination, long maxSizeBytes, ProgressListener listener)
|
||||
public void retrieveAttachment(int cdnNumber, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
downloadFromCdn(destination, String.format(Locale.US, ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener);
|
||||
final String path;
|
||||
if (cdnPath.getV2().isPresent()) {
|
||||
path = String.format(Locale.US, ATTACHMENT_ID_DOWNLOAD_PATH, cdnPath.getV2().get());
|
||||
} else {
|
||||
path = String.format(Locale.US, ATTACHMENT_KEY_DOWNLOAD_PATH, cdnPath.getV3().get());
|
||||
}
|
||||
downloadFromCdn(destination, cdnNumber, path, maxSizeBytes, listener);
|
||||
}
|
||||
|
||||
public void retrieveSticker(File destination, byte[] packId, int stickerId)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
String hexPackId = Hex.toStringCondensed(packId);
|
||||
downloadFromCdn(destination, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
downloadFromCdn(destination, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
}
|
||||
|
||||
public byte[] retrieveSticker(byte[] packId, int stickerId)
|
||||
@@ -535,7 +547,7 @@ public class PushServiceSocket {
|
||||
String hexPackId = Hex.toStringCondensed(packId);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
downloadFromCdn(output, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
downloadFromCdn(output, 0, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
|
||||
return output.toByteArray();
|
||||
}
|
||||
@@ -546,7 +558,7 @@ public class PushServiceSocket {
|
||||
String hexPackId = Hex.toStringCondensed(packId);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
downloadFromCdn(output, 0, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null);
|
||||
downloadFromCdn(output, 0, 0, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null);
|
||||
|
||||
return output.toByteArray();
|
||||
}
|
||||
@@ -611,7 +623,7 @@ public class PushServiceSocket {
|
||||
public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
downloadFromCdn(destination, path, maxSizeBytes, null);
|
||||
downloadFromCdn(destination, 0, path, maxSizeBytes, null);
|
||||
}
|
||||
|
||||
public void setProfileName(String name) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
@@ -867,10 +879,20 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentUploadAttributes getAttachmentUploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
String response = makeServiceRequest(ATTACHMENT_PATH, "GET", null);
|
||||
public AttachmentV2UploadAttributes getAttachmentV2UploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
String response = makeServiceRequest(ATTACHMENT_V2_PATH, "GET", null);
|
||||
try {
|
||||
return JsonUtil.fromJson(response, AttachmentUploadAttributes.class);
|
||||
return JsonUtil.fromJson(response, AttachmentV2UploadAttributes.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentV3UploadAttributes getAttachmentV3UploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
String response = makeServiceRequest(ATTACHMENT_V3_PATH, "GET", null);
|
||||
try {
|
||||
return JsonUtil.fromJson(response, AttachmentV3UploadAttributes.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
|
||||
@@ -890,7 +912,7 @@ public class PushServiceSocket {
|
||||
null, null);
|
||||
}
|
||||
|
||||
public Pair<Long, byte[]> uploadAttachment(PushAttachmentData attachment, AttachmentUploadAttributes uploadAttributes)
|
||||
public Pair<Long, byte[]> uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
long id = Long.parseLong(uploadAttributes.getAttachmentId());
|
||||
@@ -905,20 +927,31 @@ public class PushServiceSocket {
|
||||
return new Pair<>(id, digest);
|
||||
}
|
||||
|
||||
private void downloadFromCdn(File destination, String path, long maxSizeBytes, ProgressListener listener)
|
||||
public byte[] uploadAttachment(PushAttachmentData attachment, AttachmentV3UploadAttributes uploadAttributes) throws IOException {
|
||||
String resumableUploadUrl = getResumableUploadUrl(uploadAttributes.getSignedUploadLocation(), uploadAttributes.getHeaders());
|
||||
return uploadToCdn2(resumableUploadUrl,
|
||||
attachment.getData(),
|
||||
"application/octet-stream",
|
||||
attachment.getDataSize(),
|
||||
attachment.getOutputStreamFactory(),
|
||||
attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
}
|
||||
|
||||
private void downloadFromCdn(File destination, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
try (FileOutputStream outputStream = new FileOutputStream(destination, true)) {
|
||||
downloadFromCdn(outputStream, destination.length(), path, maxSizeBytes, listener);
|
||||
downloadFromCdn(outputStream, destination.length(), cdnNumber, path, maxSizeBytes, listener);
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadFromCdn(OutputStream outputStream, long offset, String path, long maxSizeBytes, ProgressListener listener)
|
||||
private void downloadFromCdn(OutputStream outputStream, long offset, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(cdnClients, random);
|
||||
ConnectionHolder connectionHolder = getRandom(cdnNumber == 2 ? cdn2Clients : cdnClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
@@ -1046,6 +1079,107 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private String getResumableUploadUrl(String signedUrl, Map<String, String> headers) throws IOException {
|
||||
ConnectionHolder connectionHolder = getRandom(cdn2Clients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
final HttpUrl endpointUrl = HttpUrl.get(connectionHolder.url);
|
||||
final HttpUrl signedHttpUrl;
|
||||
try {
|
||||
signedHttpUrl = HttpUrl.get(signedUrl);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Server returned a malformed signed url: " + signedUrl);
|
||||
throw new IOException("Server returned a malformed signed url", e);
|
||||
}
|
||||
|
||||
final HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme(endpointUrl.scheme())
|
||||
.host(endpointUrl.host())
|
||||
.port(endpointUrl.port())
|
||||
.encodedPath(endpointUrl.encodedPath())
|
||||
.addEncodedPathSegments(signedHttpUrl.encodedPath().substring(1))
|
||||
.encodedQuery(signedHttpUrl.encodedQuery())
|
||||
.encodedFragment(signedHttpUrl.encodedFragment());
|
||||
|
||||
Request.Builder request = new Request.Builder().url(urlBuilder.build())
|
||||
.post(RequestBody.create(null, ""));
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
request.header(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
request.header("host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
try {
|
||||
Response response;
|
||||
|
||||
try {
|
||||
response = call.execute();
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
return response.header("location");
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException("Response: " + response);
|
||||
}
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException {
|
||||
ConnectionHolder connectionHolder = getRandom(cdn2Clients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
DigestingRequestBody file = new DigestingRequestBody(data, outputStreamFactory, contentType, length, progressListener, cancelationSignal);
|
||||
Request.Builder request = new Request.Builder().url(resumableUrl)
|
||||
.put(file);
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
request.header("host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
try {
|
||||
Response response;
|
||||
|
||||
try {
|
||||
response = call.execute();
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
|
||||
if (response.isSuccessful()) return file.getTransmittedDigest();
|
||||
else throw new NonSuccessfulResponseCodeException("Response: " + response);
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String makeServiceRequest(String urlFragment, String method, String jsonBody)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
|
||||
@@ -377,7 +377,10 @@ message AttachmentPointer {
|
||||
VOICE_MESSAGE = 1;
|
||||
}
|
||||
|
||||
optional fixed64 id = 1;
|
||||
oneof attachment_identifier {
|
||||
fixed64 cdnId = 1;
|
||||
string cdnKey = 15;
|
||||
}
|
||||
optional string contentType = 2;
|
||||
optional bytes key = 3;
|
||||
optional uint32 size = 4;
|
||||
@@ -390,6 +393,8 @@ message AttachmentPointer {
|
||||
optional string caption = 11;
|
||||
optional string blurHash = 12;
|
||||
optional uint64 uploadTimestamp = 13;
|
||||
optional uint32 cdnNumber = 14;
|
||||
// Next ID: 16
|
||||
}
|
||||
|
||||
message GroupContext {
|
||||
|
||||
Reference in New Issue
Block a user