Add initial support for send/receive on CDN2.

This commit is contained in:
Ehren Kret
2020-04-05 17:32:06 -07:00
committed by Greyson Parrelli
parent 1290d0ead9
commit 37a35e8f70
32 changed files with 510 additions and 144 deletions

View File

@@ -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);
}

View File

@@ -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());
}

View File

@@ -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));
}

View File

@@ -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() {

View File

@@ -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);
}
}
}

View File

@@ -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()),

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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
{

View File

@@ -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 {