diff --git a/android/build.gradle b/android/build.gradle index 7656360c9e..1145f75f22 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -61,6 +61,11 @@ signing { sign configurations.archives } + +def sonatypeRepo = sonatypeRepo != null ? sonatypeRepo : "" +def whisperSonatypeUsername = whisperSonatypeUsername != null ? whisperSonatypeUsername : "" +def whisperSonatypePassword = whisperSonatypePassword != null ? whisperSonatypePassword : "" + uploadArchives { configuration = configurations.archives repositories.mavenDeployer { diff --git a/build.gradle b/build.gradle index dd90855165..56f5ce6a24 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ subprojects { - ext.version_number = "1.6.0" + ext.version_number = "1.8.3" ext.group_info = "org.whispersystems" ext.axolotl_version = "1.3.1" diff --git a/java/build.gradle b/java/build.gradle index 440fd91fc7..142736eac3 100644 --- a/java/build.gradle +++ b/java/build.gradle @@ -14,7 +14,7 @@ repositories { dependencies { compile 'com.google.protobuf:protobuf-java:2.5.0' - compile 'com.googlecode.libphonenumber:libphonenumber:6.1' + compile 'com.googlecode.libphonenumber:libphonenumber:7.1.0' compile 'com.fasterxml.jackson.core:jackson-databind:2.5.0' compile "org.whispersystems:axolotl-java:${axolotl_version}" @@ -35,6 +35,11 @@ signing { sign configurations.archives } + +def sonatypeRepo = sonatypeRepo != null ? sonatypeRepo : "" +def whisperSonatypeUsername = whisperSonatypeUsername != null ? whisperSonatypeUsername : "" +def whisperSonatypePassword = whisperSonatypePassword != null ? whisperSonatypePassword : "" + uploadArchives { configuration = configurations.archives repositories.mavenDeployer { diff --git a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureAccountManager.java b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureAccountManager.java index 83364f7139..bce04ef6c4 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureAccountManager.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureAccountManager.java @@ -57,6 +57,7 @@ public class TextSecureAccountManager { private final PushServiceSocket pushServiceSocket; private final String user; + private final String userAgent; /** * Construct a TextSecureAccountManager. @@ -65,12 +66,15 @@ public class TextSecureAccountManager { * @param trustStore The {@link org.whispersystems.textsecure.api.push.TrustStore} for the TextSecure server's TLS certificate. * @param user A TextSecure phone number. * @param password A TextSecure password. + * @param userAgent A string which identifies the client software. */ public TextSecureAccountManager(String url, TrustStore trustStore, - String user, String password) + String user, String password, + String userAgent) { - this.pushServiceSocket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null)); + this.pushServiceSocket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null), userAgent); this.user = user; + this.userAgent = userAgent; } /** @@ -108,27 +112,65 @@ public void requestVoiceVerificationCode() throws IOException { } /** - * Verify a TextSecure account. + * Verify a TextSecure account with a received SMS or voice verification code. * * @param verificationCode The verification code received via SMS or Voice * (see {@link #requestSmsVerificationCode} and * {@link #requestVoiceVerificationCode}). * @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, * concatenated. - * @param supportsSms Indicate whether this client is capable of supporting encrypted SMS. * @param axolotlRegistrationId A random 14-bit number that identifies this TextSecure install. * This value should remain consistent across registrations for the * same install, but probabilistically differ across registrations * for separate installs. + * @param voice A boolean that indicates whether the client supports secure voice (RedPhone) calls. * * @throws IOException */ - public void verifyAccount(String verificationCode, String signalingKey, - boolean supportsSms, int axolotlRegistrationId) + public void verifyAccountWithCode(String verificationCode, String signalingKey, int axolotlRegistrationId, boolean voice) throws IOException { - this.pushServiceSocket.verifyAccount(verificationCode, signalingKey, - supportsSms, axolotlRegistrationId); + this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey, + axolotlRegistrationId, voice); + } + + /** + * Verify a TextSecure account with a signed token from a trusted source. + * + * @param verificationToken The signed token provided by a trusted server. + + * @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, + * concatenated. + * @param axolotlRegistrationId A random 14-bit number that identifies this TextSecure install. + * This value should remain consistent across registrations for the + * same install, but probabilistically differ across registrations + * for separate installs. + * @param voice A boolean that indicates whether the client supports secure voice (RedPhone) calls. + * + * @throws IOException + */ + public void verifyAccountWithToken(String verificationToken, String signalingKey, int axolotlRegistrationId, boolean voice) + throws IOException + { + this.pushServiceSocket.verifyAccountToken(verificationToken, signalingKey, axolotlRegistrationId, voice); + } + + /** + * Refresh account attributes with server. + * + * @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, concatenated. + * @param axolotlRegistrationId A random 14-bit number that identifies this TextSecure install. + * This value should remain consistent across registrations for the same + * install, but probabilistically differ across registrations for + * separate installs. + * @param voice A boolean that indicates whether the client supports secure voice (RedPhone) + * + * @throws IOException + */ + public void setAccountAttributes(String signalingKey, int axolotlRegistrationId, boolean voice) + throws IOException + { + this.pushServiceSocket.setAccountAttributes(signalingKey, axolotlRegistrationId, voice); } /** @@ -213,6 +255,10 @@ public List getContacts(Set e164numbers) return activeTokens; } + public String getAccountVerificationToken() throws IOException { + return this.pushServiceSocket.getAccountVerificationToken(); + } + public String getNewDeviceVerificationCode() throws IOException { return this.pushServiceSocket.getNewDeviceVerificationCode(); } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java index c1b239abfb..6e653e2261 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java @@ -18,6 +18,8 @@ import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import org.whispersystems.textsecure.api.messages.TextSecureDataMessage; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; @@ -45,6 +47,7 @@ public class TextSecureMessageReceiver { private final TrustStore trustStore; private final String url; private final CredentialsProvider credentialsProvider; + private final String userAgent; /** * Construct a TextSecureMessageReceiver. @@ -57,9 +60,10 @@ public class TextSecureMessageReceiver { * @param signalingKey The 52 byte signaling key assigned to this user at registration. */ public TextSecureMessageReceiver(String url, TrustStore trustStore, - String user, String password, String signalingKey) + String user, String password, + String signalingKey, String userAgent) { - this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey)); + this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey), userAgent); } /** @@ -70,11 +74,14 @@ public TextSecureMessageReceiver(String url, TrustStore trustStore, * the server's TLS signing certificate. * @param credentials The TextSecure user's credentials. */ - public TextSecureMessageReceiver(String url, TrustStore trustStore, CredentialsProvider credentials) { + public TextSecureMessageReceiver(String url, TrustStore trustStore, + CredentialsProvider credentials, String userAgent) + { this.url = url; this.trustStore = trustStore; this.credentialsProvider = credentials; - this.socket = new PushServiceSocket(url, trustStore, credentials); + this.socket = new PushServiceSocket(url, trustStore, credentials, userAgent); + this.userAgent = userAgent; } /** @@ -91,7 +98,26 @@ public TextSecureMessageReceiver(String url, TrustStore trustStore, CredentialsP public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination) throws IOException, InvalidMessageException { - socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination); + return retrieveAttachment(pointer, destination, null); + } + + + /** + * Retrieves a TextSecure attachment. + * + * @param pointer The {@link org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer} + * received in a {@link TextSecureDataMessage}. + * @param destination The download destination for this attachment. + * @param listener An optional listener (may be null) to receive callbacks on download progress. + * + * @return An InputStream that streams the plaintext attachment contents. + * @throws IOException + * @throws InvalidMessageException + */ + public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination, ProgressListener listener) + throws IOException, InvalidMessageException + { + socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination, listener); return new AttachmentCipherInputStream(destination, pointer.getKey()); } @@ -103,7 +129,7 @@ public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File * @return A TextSecureMessagePipe for receiving TextSecure messages. */ public TextSecureMessagePipe createMessagePipe() { - WebSocketConnection webSocket = new WebSocketConnection(url, trustStore, credentialsProvider); + WebSocketConnection webSocket = new WebSocketConnection(url, trustStore, credentialsProvider, userAgent); return new TextSecureMessagePipe(webSocket, credentialsProvider); } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java index 00f0cd9353..eaee62e68f 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java @@ -88,9 +88,10 @@ public class TextSecureMessageSender { public TextSecureMessageSender(String url, TrustStore trustStore, String user, String password, AxolotlStore store, + String userAgent, Optional eventListener) { - this.socket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null)); + this.socket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null), userAgent); this.store = store; this.localAddress = new TextSecureAddress(user); this.eventListener = eventListener; @@ -335,15 +336,22 @@ private AttachmentPointer createAttachmentPointer(TextSecureAttachmentStream att PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(), attachment.getInputStream(), attachment.getLength(), + attachment.getListener(), attachmentKey); long attachmentId = socket.sendAttachment(attachmentData); - return AttachmentPointer.newBuilder() - .setContentType(attachment.getContentType()) - .setId(attachmentId) - .setKey(ByteString.copyFrom(attachmentKey)) - .build(); + AttachmentPointer.Builder builder = AttachmentPointer.newBuilder() + .setContentType(attachment.getContentType()) + .setId(attachmentId) + .setKey(ByteString.copyFrom(attachmentKey)) + .setSize((int)attachment.getLength()); + + if (attachment.getPreview().isPresent()) { + builder.setThumbnail(ByteString.copyFrom(attachment.getPreview().get())); + } + + return builder.build(); } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java b/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java index 5d54059885..8fafb0a604 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java @@ -32,6 +32,7 @@ import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.protocol.WhisperMessage; import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.messages.TextSecureAttachment; import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import org.whispersystems.textsecure.api.messages.TextSecureContent; @@ -164,7 +165,9 @@ private TextSecureDataMessage createTextSecureMessage(TextSecureEnvelope envelop attachments.add(new TextSecureAttachmentPointer(pointer.getId(), pointer.getContentType(), pointer.getKey().toByteArray(), - envelope.getRelay())); + envelope.getRelay(), + pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.absent(), + pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.absent())); } return new TextSecureDataMessage(envelope.getTimestamp(), groupInfo, attachments, diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java index 03b6437a6e..acbc09c827 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java @@ -47,9 +47,10 @@ public static Builder newStreamBuilder() { public static class Builder { - private InputStream inputStream; - private String contentType; - private long length; + private InputStream inputStream; + private String contentType; + private long length; + private ProgressListener listener; private Builder() {} @@ -68,12 +69,31 @@ public Builder withLength(long length) { return this; } + public Builder withListener(ProgressListener listener) { + this.listener = listener; + return this; + } + public TextSecureAttachmentStream build() { if (inputStream == null) throw new IllegalArgumentException("Must specify stream!"); if (contentType == null) throw new IllegalArgumentException("No content type specified!"); if (length == 0) throw new IllegalArgumentException("No length specified!"); - return new TextSecureAttachmentStream(inputStream, contentType, length); + return new TextSecureAttachmentStream(inputStream, contentType, length, listener); } } + + /** + * An interface to receive progress information on upload/download of + * an attachment. + */ + public interface ProgressListener { + /** + * Called on a progress change event. + * + * @param total The total amount to transmit/receive in bytes. + * @param progress The amount that has been transmitted/received in bytes thus far + */ + public void onAttachmentProgress(long total, long progress); + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentPointer.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentPointer.java index a86e6199d2..aad4805197 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentPointer.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentPointer.java @@ -27,15 +27,25 @@ */ public class TextSecureAttachmentPointer extends TextSecureAttachment { - private final long id; - private final byte[] key; - private final Optional relay; + private final long id; + private final byte[] key; + private final Optional relay; + private final Optional size; + private final Optional preview; public TextSecureAttachmentPointer(long id, String contentType, byte[] key, String relay) { + this(id, contentType, key, relay, Optional.absent(), Optional.absent()); + } + + public TextSecureAttachmentPointer(long id, String contentType, byte[] key, String relay, + Optional size, Optional preview) + { super(contentType); - this.id = id; - this.key = key; - this.relay = Optional.fromNullable(relay); + this.id = id; + this.key = key; + this.relay = Optional.fromNullable(relay); + this.size = size; + this.preview = preview; } public long getId() { @@ -59,4 +69,12 @@ public boolean isPointer() { public Optional getRelay() { return relay; } + + public Optional getSize() { + return size; + } + + public Optional getPreview() { + return preview; + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java index 2f57a1bcc8..a3955cfba6 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java @@ -16,6 +16,8 @@ */ package org.whispersystems.textsecure.api.messages; +import org.whispersystems.libaxolotl.util.guava.Optional; + import java.io.InputStream; /** @@ -23,13 +25,21 @@ */ public class TextSecureAttachmentStream extends TextSecureAttachment { - private final InputStream inputStream; - private final long length; + private final InputStream inputStream; + private final long length; + private final ProgressListener listener; + private final Optional preview; + + public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length, ProgressListener listener) { + this(inputStream, contentType, length, Optional.absent(), listener); + } - public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length) { + public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length, Optional preview, ProgressListener listener) { super(contentType); this.inputStream = inputStream; this.length = length; + this.listener = listener; + this.preview = preview; } @Override @@ -49,4 +59,12 @@ public InputStream getInputStream() { public long getLength() { return length; } + + public ProgressListener getListener() { + return listener; + } + + public Optional getPreview() { + return preview; + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java index c1ef46868c..8cf8dca07d 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java @@ -29,7 +29,7 @@ public DeviceContact read() throws IOException { InputStream avatarStream = new LimitedInputStream(in, avatarLength); String avatarContentType = details.getAvatar().getContentType(); - avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength)); + avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength, null)); } return new DeviceContact(number, name, avatar); diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java index 1f82df2bf2..f15fa82b8e 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java @@ -36,7 +36,7 @@ public DeviceGroup read() throws IOException { InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength); String avatarContentType = details.getAvatar().getContentType(); - avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength)); + avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength, null)); } return new DeviceGroup(id, name, members, avatar); diff --git a/java/src/main/java/org/whispersystems/textsecure/api/push/ContactTokenDetails.java b/java/src/main/java/org/whispersystems/textsecure/api/push/ContactTokenDetails.java index 3012492446..3140e7d7a8 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/push/ContactTokenDetails.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/push/ContactTokenDetails.java @@ -16,15 +16,24 @@ */ package org.whispersystems.textsecure.api.push; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * A class that represents a contact's registration state. */ public class ContactTokenDetails { + @JsonProperty private String token; + + @JsonProperty private String relay; + + @JsonProperty private String number; - private boolean supportsSms; + + @JsonProperty + private boolean voice; public ContactTokenDetails() {} @@ -43,10 +52,10 @@ public String getRelay() { } /** - * @return Whether this contact supports receiving encrypted SMS. + * @return Whether this contact supports secure voice calls. */ - public boolean isSupportsSms() { - return supportsSms; + public boolean isVoice() { + return voice; } public void setNumber(String number) { diff --git a/java/src/main/java/org/whispersystems/textsecure/api/util/PhoneNumberFormatter.java b/java/src/main/java/org/whispersystems/textsecure/api/util/PhoneNumberFormatter.java index 05ecdf145a..ce9fd9d6b6 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/util/PhoneNumberFormatter.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/util/PhoneNumberFormatter.java @@ -36,7 +36,11 @@ public class PhoneNumberFormatter { private static final String TAG = PhoneNumberFormatter.class.getSimpleName(); public static boolean isValidNumber(String number) { - return number.matches("^\\+[0-9]{10,}"); + return number.matches("^\\+[0-9]{10,}") || + number.matches("^\\+298[0-9]{6}") || + number.matches("^\\+240[0-9]{6}") || + number.matches("^\\+687[0-9]{6}") || + number.matches("^\\+689[0-9]{6}"); } private static String impreciseFormatNumber(String number, String localNumber) diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java index 5d32b48378..ada7aad212 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java @@ -24,15 +24,15 @@ public class AccountAttributes { private String signalingKey; @JsonProperty - private boolean supportsSms; + private int registrationId; @JsonProperty - private int registrationId; + private boolean voice; - public AccountAttributes(String signalingKey, boolean supportsSms, int registrationId) { + public AccountAttributes(String signalingKey, int registrationId, boolean voice) { this.signalingKey = signalingKey; - this.supportsSms = supportsSms; this.registrationId = registrationId; + this.voice = voice; } public AccountAttributes() {} @@ -41,11 +41,11 @@ public String getSignalingKey() { return signalingKey; } - public boolean isSupportsSms() { - return supportsSms; - } - public int getRegistrationId() { return registrationId; } + + public boolean isVoice() { + return voice; + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/AuthorizationToken.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/AuthorizationToken.java new file mode 100644 index 0000000000..e82a137a2b --- /dev/null +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/AuthorizationToken.java @@ -0,0 +1,13 @@ +package org.whispersystems.textsecure.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AuthorizationToken { + + @JsonProperty + private String token; + + public String getToken() { + return token; + } +} diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/ContactTokenDetailsList.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/ContactTokenDetailsList.java index 79fd91a4ed..730b23f530 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/ContactTokenDetailsList.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/ContactTokenDetailsList.java @@ -16,12 +16,15 @@ */ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + import org.whispersystems.textsecure.api.push.ContactTokenDetails; import java.util.List; public class ContactTokenDetailsList { + @JsonProperty private List contacts; public ContactTokenDetailsList() {} diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java index 11b0bcf15c..59c5c172f5 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java @@ -16,20 +16,26 @@ */ package org.whispersystems.textsecure.internal.push; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; + import java.io.InputStream; public class PushAttachmentData { - private final String contentType; - private final InputStream data; - private final long dataSize; - private final byte[] key; + private final String contentType; + private final InputStream data; + private final long dataSize; + private final byte[] key; + private final ProgressListener listener; - public PushAttachmentData(String contentType, InputStream data, long dataSize, byte[] key) { + public PushAttachmentData(String contentType, InputStream data, long dataSize, + ProgressListener listener, byte[] key) + { this.contentType = contentType; this.data = data; this.dataSize = dataSize; this.key = key; + this.listener = listener; } public String getContentType() { @@ -47,4 +53,8 @@ public long getDataSize() { public byte[] getKey() { return key; } + + public ProgressListener getListener() { + return listener; + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java index d62c9b7b72..7a76284410 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java @@ -17,6 +17,12 @@ package org.whispersystems.textsecure.internal.push; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.whispersystems.libaxolotl.IdentityKey; @@ -27,10 +33,11 @@ import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.crypto.AttachmentCipherOutputStream; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo; import org.whispersystems.textsecure.api.push.ContactTokenDetails; -import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.SignedPreKeyEntity; +import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.TrustStore; import org.whispersystems.textsecure.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.textsecure.api.push.exceptions.ExpectationFailedException; @@ -77,8 +84,11 @@ public class PushServiceSocket { private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s"; private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s"; - private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s"; + private static final String VERIFY_ACCOUNT_CODE_PATH = "/v1/accounts/code/%s"; + private static final String VERIFY_ACCOUNT_TOKEN_PATH = "/v1/accounts/token/%s"; private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/"; + private static final String REQUEST_TOKEN_PATH = "/v1/accounts/token"; + private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/"; private static final String PREKEY_METADATA_PATH = "/v2/keys/"; private static final String PREKEY_PATH = "/v2/keys/%s"; @@ -96,17 +106,17 @@ public class PushServiceSocket { private static final String RECEIPT_PATH = "/v1/receipt/%s/%d"; private static final String ATTACHMENT_PATH = "/v1/attachments/%s"; - private static final boolean ENFORCE_SSL = true; - private final String serviceUrl; private final TrustManager[] trustManagers; private final CredentialsProvider credentialsProvider; + private final String userAgent; - public PushServiceSocket(String serviceUrl, TrustStore trustStore, CredentialsProvider credentialsProvider) + public PushServiceSocket(String serviceUrl, TrustStore trustStore, CredentialsProvider credentialsProvider, String userAgent) { this.serviceUrl = serviceUrl; this.credentialsProvider = credentialsProvider; this.trustManagers = BlacklistingTrustManager.createFor(trustStore); + this.userAgent = userAgent; } public void createAccount(boolean voice) throws IOException { @@ -114,15 +124,32 @@ public void createAccount(boolean voice) throws IOException { makeRequest(String.format(path, credentialsProvider.getUser()), "GET", null); } - public void verifyAccount(String verificationCode, String signalingKey, - boolean supportsSms, int registrationId) + public void verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean voice) + throws IOException + { + AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, voice); + makeRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), + "PUT", JsonUtil.toJson(signalingKeyEntity)); + } + + public void verifyAccountToken(String verificationToken, String signalingKey, int registrationId, boolean voice) throws IOException { - AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId); - makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode), + AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, voice); + makeRequest(String.format(VERIFY_ACCOUNT_TOKEN_PATH, verificationToken), "PUT", JsonUtil.toJson(signalingKeyEntity)); } + public void setAccountAttributes(String signalingKey, int registrationId, boolean voice) throws IOException { + AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, voice); + makeRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes)); + } + + public String getAccountVerificationToken() throws IOException { + String responseText = makeRequest(REQUEST_TOKEN_PATH, "GET", null); + return JsonUtil.fromJson(responseText, AuthorizationToken.class).getToken(); + } + public String getNewDeviceVerificationCode() throws IOException { String responseText = makeRequest(PROVISIONING_CODE_PATH, "GET", null); return JsonUtil.fromJson(responseText, DeviceCode.class).getVerificationCode(); @@ -258,8 +285,6 @@ public List getPreKeys(TextSecureAddress destination, int deviceId } return bundles; - } catch (JsonUtil.JsonParseException e) { - throw new IOException(e); } catch (NotFoundException nfe) { throw new UnregisteredUserException(destination.getNumber(), nfe); } @@ -300,8 +325,6 @@ public PreKeyBundle getPreKey(TextSecureAddress destination, int deviceId) throw return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey, signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey()); - } catch (JsonUtil.JsonParseException e) { - throw new IOException(e); } catch (NotFoundException nfe) { throw new UnregisteredUserException(destination.getNumber(), nfe); } @@ -335,12 +358,12 @@ public long sendAttachment(PushAttachmentData attachment) throws IOException { Log.w(TAG, "Got attachment content location: " + attachmentKey.getLocation()); uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(), - attachment.getDataSize(), attachment.getKey()); + attachment.getDataSize(), attachment.getKey(), attachment.getListener()); return attachmentKey.getId(); } - public void retrieveAttachment(String relay, long attachmentId, File destination) throws IOException { + public void retrieveAttachment(String relay, long attachmentId, File destination, ProgressListener listener) throws IOException { String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId)); if (!Util.isEmpty(relay)) { @@ -352,17 +375,22 @@ public void retrieveAttachment(String relay, long attachmentId, File destination Log.w(TAG, "Attachment: " + attachmentId + " is at: " + descriptor.getLocation()); - downloadExternalFile(descriptor.getLocation(), destination); + downloadExternalFile(descriptor.getLocation(), destination, listener); } public List retrieveDirectory(Set contactTokens) throws NonSuccessfulResponseCodeException, PushNetworkException { - ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens)); - String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", JsonUtil.toJson(contactTokenList)); - ContactTokenDetailsList activeTokens = JsonUtil.fromJson(response, ContactTokenDetailsList.class); + try { + ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens)); + String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", JsonUtil.toJson(contactTokenList)); + ContactTokenDetailsList activeTokens = JsonUtil.fromJson(response, ContactTokenDetailsList.class); - return activeTokens.getContacts(); + return activeTokens.getContacts(); + } catch (IOException e) { + Log.w(TAG, e); + throw new NonSuccessfulResponseCodeException("Unable to parse entity"); + } } public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException { @@ -374,7 +402,7 @@ public ContactTokenDetails getContactTokenDetails(String contactToken) throws IO } } - private void downloadExternalFile(String url, File localDestination) + private void downloadExternalFile(String url, File localDestination, ProgressListener listener) throws IOException { URL downloadUrl = new URL(url); @@ -388,13 +416,19 @@ private void downloadExternalFile(String url, File localDestination) throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode()); } - OutputStream output = new FileOutputStream(localDestination); - InputStream input = connection.getInputStream(); - byte[] buffer = new byte[4096]; - int read; + OutputStream output = new FileOutputStream(localDestination); + InputStream input = connection.getInputStream(); + byte[] buffer = new byte[4096]; + int contentLength = connection.getContentLength(); + int read,totalRead = 0; while ((read = input.read(buffer)) != -1) { output.write(buffer, 0, read); + totalRead += read; + + if (listener != null) { + listener.onAttachmentProgress(contentLength, totalRead); + } } output.close(); @@ -406,7 +440,8 @@ private void downloadExternalFile(String url, File localDestination) } } - private void uploadAttachment(String method, String url, InputStream data, long dataSize, byte[] key) + private void uploadAttachment(String method, String url, InputStream data, + long dataSize, byte[] key, ProgressListener listener) throws IOException { URL uploadUrl = new URL(url); @@ -427,9 +462,21 @@ private void uploadAttachment(String method, String url, InputStream data, long try { OutputStream stream = connection.getOutputStream(); AttachmentCipherOutputStream out = new AttachmentCipherOutputStream(key, stream); + byte[] buffer = new byte[4096]; + int read, written = 0; - Util.copy(data, out); + while ((read = data.read(buffer)) != -1) { + out.write(buffer, 0, read); + written += read; + + if (listener != null) { + listener.onAttachmentProgress(dataSize, written); + } + } + + data.close(); out.flush(); + out.close(); if (connection.getResponseCode() != 200) { throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); @@ -442,65 +489,65 @@ private void uploadAttachment(String method, String url, InputStream data, long private String makeRequest(String urlFragment, String method, String body) throws NonSuccessfulResponseCodeException, PushNetworkException { - HttpURLConnection connection = makeBaseRequest(urlFragment, method, body); - - try { - String response = Util.readFully(connection.getInputStream()); - connection.disconnect(); - - return response; - } catch (IOException ioe) { - throw new PushNetworkException(ioe); - } - } + Response response = getConnection(urlFragment, method, body); - private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body) - throws NonSuccessfulResponseCodeException, PushNetworkException - { - HttpURLConnection connection = getConnection(urlFragment, method, body); - int responseCode; - String responseMessage; - String response; + int responseCode; + String responseMessage; + String responseBody; try { - responseCode = connection.getResponseCode(); - responseMessage = connection.getResponseMessage(); + responseCode = response.code(); + responseMessage = response.message(); + responseBody = response.body().string(); } catch (IOException ioe) { throw new PushNetworkException(ioe); } switch (responseCode) { case 413: - connection.disconnect(); throw new RateLimitException("Rate limit exceeded: " + responseCode); case 401: case 403: - connection.disconnect(); throw new AuthorizationFailedException("Authorization failed!"); case 404: - connection.disconnect(); throw new NotFoundException("Not found"); case 409: + MismatchedDevices mismatchedDevices; + try { - response = Util.readFully(connection.getErrorStream()); + mismatchedDevices = JsonUtil.fromJson(responseBody, MismatchedDevices.class); + } catch (JsonProcessingException e) { + Log.w(TAG, e); + throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } catch (IOException e) { throw new PushNetworkException(e); } - throw new MismatchedDevicesException(JsonUtil.fromJson(response, MismatchedDevices.class)); + + throw new MismatchedDevicesException(mismatchedDevices); case 410: + StaleDevices staleDevices; + try { - response = Util.readFully(connection.getErrorStream()); + staleDevices = JsonUtil.fromJson(responseBody, StaleDevices.class); + } catch (JsonProcessingException e) { + throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } catch (IOException e) { throw new PushNetworkException(e); } - throw new StaleDevicesException(JsonUtil.fromJson(response, StaleDevices.class)); + + throw new StaleDevicesException(staleDevices); case 411: + DeviceLimit deviceLimit; + try { - response = Util.readFully(connection.getErrorStream()); + deviceLimit = JsonUtil.fromJson(responseBody, DeviceLimit.class); + } catch (JsonProcessingException e) { + throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } catch (IOException e) { throw new PushNetworkException(e); } - throw new DeviceLimitExceededException(JsonUtil.fromJson(response, DeviceLimit.class)); + + throw new DeviceLimitExceededException(deviceLimit); case 417: throw new ExpectationFailedException(); } @@ -510,48 +557,41 @@ private HttpURLConnection makeBaseRequest(String urlFragment, String method, Str responseMessage); } - return connection; + return responseBody; } - private HttpURLConnection getConnection(String urlFragment, String method, String body) + private Response getConnection(String urlFragment, String method, String body) throws PushNetworkException { try { + Log.w(TAG, "Push service URL: " + serviceUrl); + Log.w(TAG, "Opening URL: " + String.format("%s%s", serviceUrl, urlFragment)); + SSLContext context = SSLContext.getInstance("TLS"); context.init(null, trustManagers, null); - URL url = new URL(String.format("%s%s", serviceUrl, urlFragment)); - Log.w(TAG, "Push service URL: " + serviceUrl); - Log.w(TAG, "Opening URL: " + url); + OkHttpClient okHttpClient = new OkHttpClient(); + okHttpClient.setSslSocketFactory(context.getSocketFactory()); + okHttpClient.setHostnameVerifier(new StrictHostnameVerifier()); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + Request.Builder request = new Request.Builder(); + request.url(String.format("%s%s", serviceUrl, urlFragment)); - if (ENFORCE_SSL) { - ((HttpsURLConnection) connection).setSSLSocketFactory(context.getSocketFactory()); - ((HttpsURLConnection) connection).setHostnameVerifier(new StrictHostnameVerifier()); + if (body != null) { + request.method(method, RequestBody.create(MediaType.parse("application/json"), body)); + } else { + request.method(method, null); } - connection.setRequestMethod(method); - connection.setRequestProperty("Content-Type", "application/json"); - if (credentialsProvider.getPassword() != null) { - connection.setRequestProperty("Authorization", getAuthorizationHeader()); - } - - if (body != null) { - connection.setDoOutput(true); + request.addHeader("Authorization", getAuthorizationHeader()); } - connection.connect(); - - if (body != null) { - Log.w(TAG, method + " -- " + body); - OutputStream out = connection.getOutputStream(); - out.write(body.getBytes()); - out.close(); + if (userAgent != null) { + request.addHeader("X-Signal-Agent", userAgent); } - return connection; + return okHttpClient.newCall(request.build()).execute(); } catch (IOException e) { throw new PushNetworkException(e); } catch (NoSuchAlgorithmException | KeyManagementException e) { diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/TextSecureProtos.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/TextSecureProtos.java index 652baefabb..ee3d6b2861 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/TextSecureProtos.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/TextSecureProtos.java @@ -6480,6 +6480,26 @@ public interface AttachmentPointerOrBuilder * optional bytes key = 3; */ com.google.protobuf.ByteString getKey(); + + // optional uint32 size = 4; + /** + * optional uint32 size = 4; + */ + boolean hasSize(); + /** + * optional uint32 size = 4; + */ + int getSize(); + + // optional bytes thumbnail = 5; + /** + * optional bytes thumbnail = 5; + */ + boolean hasThumbnail(); + /** + * optional bytes thumbnail = 5; + */ + com.google.protobuf.ByteString getThumbnail(); } /** * Protobuf type {@code textsecure.AttachmentPointer} @@ -6547,6 +6567,16 @@ private AttachmentPointer( key_ = input.readBytes(); break; } + case 32: { + bitField0_ |= 0x00000008; + size_ = input.readUInt32(); + break; + } + case 42: { + bitField0_ |= 0x00000010; + thumbnail_ = input.readBytes(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -6662,10 +6692,44 @@ public com.google.protobuf.ByteString getKey() { return key_; } + // optional uint32 size = 4; + public static final int SIZE_FIELD_NUMBER = 4; + private int size_; + /** + * optional uint32 size = 4; + */ + public boolean hasSize() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + /** + * optional uint32 size = 4; + */ + public int getSize() { + return size_; + } + + // optional bytes thumbnail = 5; + public static final int THUMBNAIL_FIELD_NUMBER = 5; + private com.google.protobuf.ByteString thumbnail_; + /** + * optional bytes thumbnail = 5; + */ + public boolean hasThumbnail() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + /** + * optional bytes thumbnail = 5; + */ + public com.google.protobuf.ByteString getThumbnail() { + return thumbnail_; + } + private void initFields() { id_ = 0L; contentType_ = ""; key_ = com.google.protobuf.ByteString.EMPTY; + size_ = 0; + thumbnail_ = com.google.protobuf.ByteString.EMPTY; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -6688,6 +6752,12 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (((bitField0_ & 0x00000004) == 0x00000004)) { output.writeBytes(3, key_); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + output.writeUInt32(4, size_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + output.writeBytes(5, thumbnail_); + } getUnknownFields().writeTo(output); } @@ -6709,6 +6779,14 @@ public int getSerializedSize() { size += com.google.protobuf.CodedOutputStream .computeBytesSize(3, key_); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt32Size(4, size_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(5, thumbnail_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -6831,6 +6909,10 @@ public Builder clear() { bitField0_ = (bitField0_ & ~0x00000002); key_ = com.google.protobuf.ByteString.EMPTY; bitField0_ = (bitField0_ & ~0x00000004); + size_ = 0; + bitField0_ = (bitField0_ & ~0x00000008); + thumbnail_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000010); return this; } @@ -6871,6 +6953,14 @@ public org.whispersystems.textsecure.internal.push.TextSecureProtos.AttachmentPo to_bitField0_ |= 0x00000004; } result.key_ = key_; + if (((from_bitField0_ & 0x00000008) == 0x00000008)) { + to_bitField0_ |= 0x00000008; + } + result.size_ = size_; + if (((from_bitField0_ & 0x00000010) == 0x00000010)) { + to_bitField0_ |= 0x00000010; + } + result.thumbnail_ = thumbnail_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -6898,6 +6988,12 @@ public Builder mergeFrom(org.whispersystems.textsecure.internal.push.TextSecureP if (other.hasKey()) { setKey(other.getKey()); } + if (other.hasSize()) { + setSize(other.getSize()); + } + if (other.hasThumbnail()) { + setThumbnail(other.getThumbnail()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -7068,6 +7164,75 @@ public Builder clearKey() { return this; } + // optional uint32 size = 4; + private int size_ ; + /** + * optional uint32 size = 4; + */ + public boolean hasSize() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + /** + * optional uint32 size = 4; + */ + public int getSize() { + return size_; + } + /** + * optional uint32 size = 4; + */ + public Builder setSize(int value) { + bitField0_ |= 0x00000008; + size_ = value; + onChanged(); + return this; + } + /** + * optional uint32 size = 4; + */ + public Builder clearSize() { + bitField0_ = (bitField0_ & ~0x00000008); + size_ = 0; + onChanged(); + return this; + } + + // optional bytes thumbnail = 5; + private com.google.protobuf.ByteString thumbnail_ = com.google.protobuf.ByteString.EMPTY; + /** + * optional bytes thumbnail = 5; + */ + public boolean hasThumbnail() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + /** + * optional bytes thumbnail = 5; + */ + public com.google.protobuf.ByteString getThumbnail() { + return thumbnail_; + } + /** + * optional bytes thumbnail = 5; + */ + public Builder setThumbnail(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000010; + thumbnail_ = value; + onChanged(); + return this; + } + /** + * optional bytes thumbnail = 5; + */ + public Builder clearThumbnail() { + bitField0_ = (bitField0_ & ~0x00000010); + thumbnail_ = getDefaultInstance().getThumbnail(); + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:textsecure.AttachmentPointer) } @@ -11161,23 +11326,24 @@ public org.whispersystems.textsecure.internal.push.TextSecureProtos.GroupDetails "\004blob\030\001 \001(\0132\035.textsecure.AttachmentPoint" + "er\032l\n\007Request\0222\n\004type\030\001 \001(\0162$.textsecure" + ".SyncMessage.Request.Type\"-\n\004Type\022\013\n\007UNK" + - "NOWN\020\000\022\014\n\010CONTACTS\020\001\022\n\n\006GROUPS\020\002\"A\n\021Atta" + + "NOWN\020\000\022\014\n\010CONTACTS\020\001\022\n\n\006GROUPS\020\002\"b\n\021Atta" + "chmentPointer\022\n\n\002id\030\001 \001(\006\022\023\n\013contentType" + - "\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\"\315\001\n\014GroupContext\022\n\n\002" + - "id\030\001 \001(\014\022+\n\004type\030\002 \001(\0162\035.textsecure.Grou", - "pContext.Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004" + - " \003(\t\022-\n\006avatar\030\005 \001(\0132\035.textsecure.Attach" + - "mentPointer\"6\n\004Type\022\013\n\007UNKNOWN\020\000\022\n\n\006UPDA" + - "TE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\"\220\001\n\016ContactD" + - "etails\022\016\n\006number\030\001 \001(\t\022\014\n\004name\030\002 \001(\t\0221\n\006" + - "avatar\030\003 \001(\0132!.textsecure.ContactDetails" + - ".Avatar\032-\n\006Avatar\022\023\n\013contentType\030\001 \001(\t\022\016" + - "\n\006length\030\002 \001(\r\"\231\001\n\014GroupDetails\022\n\n\002id\030\001 " + - "\001(\014\022\014\n\004name\030\002 \001(\t\022\017\n\007members\030\003 \003(\t\022/\n\006av" + - "atar\030\004 \001(\0132\037.textsecure.GroupDetails.Ava", - "tar\032-\n\006Avatar\022\023\n\013contentType\030\001 \001(\t\022\016\n\006le" + - "ngth\030\002 \001(\rB?\n+org.whispersystems.textsec" + - "ure.internal.pushB\020TextSecureProtos" + "\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004size\030\004 \001(\r\022\021\n\tthu" + + "mbnail\030\005 \001(\014\"\315\001\n\014GroupContext\022\n\n\002id\030\001 \001(", + "\014\022+\n\004type\030\002 \001(\0162\035.textsecure.GroupContex" + + "t.Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\022-\n" + + "\006avatar\030\005 \001(\0132\035.textsecure.AttachmentPoi" + + "nter\"6\n\004Type\022\013\n\007UNKNOWN\020\000\022\n\n\006UPDATE\020\001\022\013\n" + + "\007DELIVER\020\002\022\010\n\004QUIT\020\003\"\220\001\n\016ContactDetails\022" + + "\016\n\006number\030\001 \001(\t\022\014\n\004name\030\002 \001(\t\0221\n\006avatar\030" + + "\003 \001(\0132!.textsecure.ContactDetails.Avatar" + + "\032-\n\006Avatar\022\023\n\013contentType\030\001 \001(\t\022\016\n\006lengt" + + "h\030\002 \001(\r\"\231\001\n\014GroupDetails\022\n\n\002id\030\001 \001(\014\022\014\n\004" + + "name\030\002 \001(\t\022\017\n\007members\030\003 \003(\t\022/\n\006avatar\030\004 ", + "\001(\0132\037.textsecure.GroupDetails.Avatar\032-\n\006" + + "Avatar\022\023\n\013contentType\030\001 \001(\t\022\016\n\006length\030\002 " + + "\001(\rB?\n+org.whispersystems.textsecure.int" + + "ernal.pushB\020TextSecureProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -11237,7 +11403,7 @@ public com.google.protobuf.ExtensionRegistry assignDescriptors( internal_static_textsecure_AttachmentPointer_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_textsecure_AttachmentPointer_descriptor, - new java.lang.String[] { "Id", "ContentType", "Key", }); + new java.lang.String[] { "Id", "ContentType", "Key", "Size", "Thumbnail", }); internal_static_textsecure_GroupContext_descriptor = getDescriptor().getMessageTypes().get(5); internal_static_textsecure_GroupContext_fieldAccessorTable = new diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java b/java/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java index 104a79fc3a..37f50dd0bd 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; @@ -36,21 +37,12 @@ public static String toJson(Object object) { } } - public static T fromJson(String json, Class clazz) { - try { - return objectMapper.readValue(json, clazz); - } catch (IOException e) { - Log.w(TAG, e); - throw new JsonParseException(e); - } + public static T fromJson(String json, Class clazz) + throws IOException + { + return objectMapper.readValue(json, clazz); } - - public static class JsonParseException extends RuntimeException { - public JsonParseException(Exception e) { - super(e); - } - } - + public static class IdentityKeySerializer extends JsonSerializer { @Override public void serialize(IdentityKey value, JsonGenerator gen, SerializerProvider serializers) diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java b/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java index 15917f987a..d08d033f94 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java @@ -96,7 +96,6 @@ public static void readFully(InputStream in, byte[] buffer) throws IOException { } } - public static void copy(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[4096]; int read; @@ -124,19 +123,4 @@ public static void wait(Object lock, long millis) { throw new AssertionError(e); } } - - public static byte[] toVarint64(long value) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - while (true) { - if ((value & ~0x7FL) == 0) { - out.write((int) value); - return out.toByteArray(); - } else { - out.write(((int) value & 0x7F) | 0x80); - value >>>= 7; - } - } - } - } diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/websocket/OkHttpClientWrapper.java b/java/src/main/java/org/whispersystems/textsecure/internal/websocket/OkHttpClientWrapper.java index a439565364..9a60e17f5c 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/websocket/OkHttpClientWrapper.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/websocket/OkHttpClientWrapper.java @@ -31,6 +31,7 @@ public class OkHttpClientWrapper implements WebSocketListener { private final TrustStore trustStore; private final CredentialsProvider credentialsProvider; private final WebSocketEventListener listener; + private final String userAgent; private WebSocket webSocket; private boolean closed; @@ -38,6 +39,7 @@ public class OkHttpClientWrapper implements WebSocketListener { public OkHttpClientWrapper(String uri, TrustStore trustStore, CredentialsProvider credentialsProvider, + String userAgent, WebSocketEventListener listener) { Log.w(TAG, "Connecting to: " + uri); @@ -45,6 +47,7 @@ public OkHttpClientWrapper(String uri, TrustStore trustStore, this.uri = uri; this.trustStore = trustStore; this.credentialsProvider = credentialsProvider; + this.userAgent = userAgent; this.listener = listener; } @@ -127,7 +130,13 @@ private synchronized WebSocket newSocket(int timeout, TimeUnit unit) { okHttpClient.setReadTimeout(timeout, unit); okHttpClient.setConnectTimeout(timeout, unit); - return WebSocket.newWebSocket(okHttpClient, new Request.Builder().url(filledUri).build()); + Request.Builder requestBuilder = new Request.Builder().url(filledUri); + + if (userAgent != null) { + requestBuilder.addHeader("X-Signal-Agent", userAgent); + } + + return WebSocket.newWebSocket(okHttpClient, requestBuilder.build()); } private SSLSocketFactory createTlsSocketFactory(TrustStore trustStore) { diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/websocket/WebSocketConnection.java b/java/src/main/java/org/whispersystems/textsecure/internal/websocket/WebSocketConnection.java index 3b4a67f243..46281dbab4 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/websocket/WebSocketConnection.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/websocket/WebSocketConnection.java @@ -27,13 +27,15 @@ public class WebSocketConnection implements WebSocketEventListener { private final String wsUri; private final TrustStore trustStore; private final CredentialsProvider credentialsProvider; + private final String userAgent; private OkHttpClientWrapper client; private KeepAliveSender keepAliveSender; - public WebSocketConnection(String httpUri, TrustStore trustStore, CredentialsProvider credentialsProvider) { + public WebSocketConnection(String httpUri, TrustStore trustStore, CredentialsProvider credentialsProvider, String userAgent) { this.trustStore = trustStore; this.credentialsProvider = credentialsProvider; + this.userAgent = userAgent; this.wsUri = httpUri.replace("https://", "wss://") .replace("http://", "ws://") + "/v1/websocket/?login=%s&password=%s"; } @@ -42,7 +44,7 @@ public synchronized void connect() { Log.w(TAG, "WSC connect()..."); if (client == null) { - client = new OkHttpClientWrapper(wsUri, trustStore, credentialsProvider, this); + client = new OkHttpClientWrapper(wsUri, trustStore, credentialsProvider, userAgent, this); client.connect(KEEPALIVE_TIMEOUT_SECONDS + 10, TimeUnit.SECONDS); } } diff --git a/protobuf/TextSecure.proto b/protobuf/TextSecure.proto index d561175280..91514c4bc9 100644 --- a/protobuf/TextSecure.proto +++ b/protobuf/TextSecure.proto @@ -72,6 +72,8 @@ message AttachmentPointer { optional fixed64 id = 1; optional string contentType = 2; optional bytes key = 3; + optional uint32 size = 4; + optional bytes thumbnail = 5; } message GroupContext {