diff --git a/library/src/org/whispersystems/textsecure/Release.java b/library/src/org/whispersystems/textsecure/Release.java index 13d5b78824..07cebf4ffa 100644 --- a/library/src/org/whispersystems/textsecure/Release.java +++ b/library/src/org/whispersystems/textsecure/Release.java @@ -1,7 +1,7 @@ package org.whispersystems.textsecure; public class Release { - public static final String PUSH_SERVICE_URL = "https://gcm.textsecure.whispersystems.org"; -// public static final String PUSH_SERVICE_URL = "http://192.168.1.135:8080"; - public static final boolean ENFORCE_SSL = true; +// public static final String PUSH_SERVICE_URL = "https://gcm.textsecure.whispersystems.org"; + public static final String PUSH_SERVICE_URL = "http://192.168.1.135:8080"; + public static final boolean ENFORCE_SSL = false; } diff --git a/library/src/org/whispersystems/textsecure/directory/NumberFilter.java b/library/src/org/whispersystems/textsecure/directory/NumberFilter.java index f59828302f..290306f599 100644 --- a/library/src/org/whispersystems/textsecure/directory/NumberFilter.java +++ b/library/src/org/whispersystems/textsecure/directory/NumberFilter.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.util.List; /** * Handles providing lookups, serializing, and deserializing the RedPhone directory. @@ -81,6 +82,26 @@ public class NumberFilter { } } + public synchronized boolean containsNumbers(List numbers) { + try { + if (bloomFilter == null) return false; + if (numbers == null || numbers.size() == 0) return false; + + BloomFilter filter = new BloomFilter(bloomFilter, hashCount); + + for (String number : numbers) { + if (!filter.contains(number)) { + return false; + } + } + + return true; + } catch (IOException ioe) { + Log.w("NumberFilter", ioe); + return false; + } + } + public synchronized void update(File bloomFilter, long capacity, int hashCount, String version) { if (this.bloomFilter != null) diff --git a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java index f350a9529c..ef082c706b 100644 --- a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java +++ b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java @@ -9,6 +9,19 @@ public class OutgoingPushMessage { private String messageText; private List attachments; + public OutgoingPushMessage(String destination, String messageText) { + this.destinations = new LinkedList(); + this.attachments = new LinkedList(); + this.messageText = messageText; + this.destinations.add(destination); + } + + public OutgoingPushMessage(List destinations, String messageText) { + this.destinations = destinations; + this.messageText = messageText; + this.attachments = new LinkedList(); + } + public OutgoingPushMessage(List destinations, String messageText, List attachments) { @@ -17,13 +30,6 @@ public class OutgoingPushMessage { this.attachments = attachments; } - public OutgoingPushMessage(String destination, String messageText) { - this.destinations = new LinkedList(); - this.attachments = new LinkedList(); - this.messageText = messageText; - this.destinations.add(destination); - } - public List getDestinations() { return destinations; } diff --git a/library/src/org/whispersystems/textsecure/push/PushAttachment.java b/library/src/org/whispersystems/textsecure/push/PushAttachment.java new file mode 100644 index 0000000000..ffcec2e0e1 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/push/PushAttachment.java @@ -0,0 +1,21 @@ +package org.whispersystems.textsecure.push; + +public class PushAttachment { + + private final String contentType; + private final byte[] data; + + public PushAttachment(String contentType, byte[] data) { + this.contentType = contentType; + this.data = data; + } + + public String getContentType() { + return contentType; + } + + public byte[] getData() { + return data; + } + +} diff --git a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java index b071de349f..9be43853b1 100644 --- a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java +++ b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java @@ -3,6 +3,7 @@ package org.whispersystems.textsecure.push; import android.content.Context; import android.util.Base64; import android.util.Log; +import android.util.Pair; import com.google.thoughtcrimegson.Gson; import org.whispersystems.textsecure.R; @@ -22,12 +23,16 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.LinkedList; +import java.util.List; import java.util.zip.GZIPInputStream; public class PushServiceSocket { @@ -39,6 +44,7 @@ public class PushServiceSocket { private static final String DIRECTORY_PATH = "/v1/directory/"; private static final String MESSAGE_PATH = "/v1/messages/"; + private static final String ATTACHMENT_PATH = "/v1/attachments/%s"; private final String localNumber; private final String password; @@ -72,6 +78,26 @@ public class PushServiceSocket { throws IOException, RateLimitException { OutgoingPushMessage message = new OutgoingPushMessage(recipient, messageText); + sendMessage(message); + } + + public void sendMessage(List recipients, String messageText) + throws IOException, RateLimitException + { + OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText); + sendMessage(message); + } + + public void sendMessage(List recipients, String messageText, + List attachments) + throws IOException, RateLimitException + { + List attachmentIds = sendAttachments(attachments); + OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds); + sendMessage(message); + } + + private void sendMessage(OutgoingPushMessage message) throws IOException, RateLimitException { String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(message)); GcmMessageResponse response = new Gson().fromJson(responseText, GcmMessageResponse.class); @@ -79,12 +105,41 @@ public class PushServiceSocket { throw new IOException("Got send failure: " + response.getFailure().get(0)); } + private List sendAttachments(List attachments) + throws IOException, RateLimitException + { + List attachmentIds = new LinkedList(); + + for (PushAttachment attachment : attachments) { + attachmentIds.add(sendAttachment(attachment)); + } + + return attachmentIds; + } + + private String sendAttachment(PushAttachment attachment) throws IOException, RateLimitException { + Pair response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, ""), + "GET", null, "Content-Location"); + + String contentLocation = response.first; + Log.w("PushServiceSocket", "Got attachment content location: " + contentLocation); + + if (contentLocation == null) { + throw new IOException("Server failed to allocate an attachment key!"); + } + + uploadExternalFile("PUT", contentLocation, attachment.getData()); + + return new Gson().fromJson(response.second, AttachmentDescriptor.class).getId(); + } + public void retrieveDirectory(Context context ) { try { DirectoryDescriptor directoryDescriptor = new Gson().fromJson(makeRequest(DIRECTORY_PATH, "GET", null), DirectoryDescriptor.class); - File directoryData = downloadExternalFile(context, directoryDescriptor.getUrl()); + File directoryData = File.createTempFile("directory", ".dat", context.getFilesDir()); + downloadExternalFile(directoryDescriptor.getUrl(), directoryData); NumberFilter.getInstance(context).update(directoryData, directoryDescriptor.getCapacity(), @@ -98,32 +153,81 @@ public class PushServiceSocket { } } - private File downloadExternalFile(Context context, String url) throws IOException { - File download = File.createTempFile("directory", ".dat", context.getFilesDir()); - URL downloadUrl = new URL(url); - HttpsURLConnection connection = (HttpsURLConnection)downloadUrl.openConnection(); + private void downloadExternalFile(String url, File localDestination) + throws IOException + { + URL downloadUrl = new URL(url); + HttpsURLConnection connection = (HttpsURLConnection) downloadUrl.openConnection(); connection.setDoInput(true); - if (connection.getResponseCode() != 200) { - throw new IOException("Bad response: " + connection.getResponseCode()); + try { + if (connection.getResponseCode() != 200) { + throw new IOException("Bad response: " + connection.getResponseCode()); + } + + OutputStream output = new FileOutputStream(localDestination); + InputStream input = new GZIPInputStream(connection.getInputStream()); + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + + output.close(); + } finally { + connection.disconnect(); } + } - OutputStream output = new FileOutputStream(download); - InputStream input = new GZIPInputStream(connection.getInputStream()); - byte[] buffer = new byte[4096]; - int read; + private void uploadExternalFile(String method, String url, byte[] data) + throws IOException + { + URL uploadUrl = new URL(url); + HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod(method); + connection.setRequestProperty("Content-Type", "application/octet-stream"); + connection.connect(); - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); + try { + OutputStream out = connection.getOutputStream(); + out.write(data); + out.close(); + + if (connection.getResponseCode() != 200) { + throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); + } + } finally { + connection.disconnect(); } + } - output.close(); + private Pair makeRequestForResponseHeader(String urlFragment, String method, + String body, String responseHeader) + throws IOException, RateLimitException + { + HttpURLConnection connection = makeBaseRequest(urlFragment, method, body); + String response = Util.readFully(connection.getInputStream()); + String headerValue = connection.getHeaderField(responseHeader); + connection.disconnect(); - return download; + return new Pair(headerValue, response); } private String makeRequest(String urlFragment, String method, String body) throws IOException, RateLimitException + { + HttpURLConnection connection = makeBaseRequest(urlFragment, method, body); + String response = Util.readFully(connection.getInputStream()); + + connection.disconnect(); + + return response; + } + + private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body) + throws IOException, RateLimitException { HttpURLConnection connection = getConnection(urlFragment, method); @@ -148,7 +252,7 @@ public class PushServiceSocket { throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); } - return Util.readFully(connection.getInputStream()); + return connection; } private HttpURLConnection getConnection(String urlFragment, String method) throws IOException { @@ -220,4 +324,16 @@ public class PushServiceSocket { } } + private class AttachmentDescriptor { + private String id; + + public AttachmentDescriptor(String id) { + this.id = id; + } + + public String getId() { + return id; + } + } + } diff --git a/src/org/thoughtcrime/securesms/service/MmsSender.java b/src/org/thoughtcrime/securesms/service/MmsSender.java index 398efa5553..e0be5d9457 100644 --- a/src/org/thoughtcrime/securesms/service/MmsSender.java +++ b/src/org/thoughtcrime/securesms/service/MmsSender.java @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler; import org.thoughtcrime.securesms.transport.MmsTransport; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; +import org.thoughtcrime.securesms.transport.UniversalTransport; import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.pdu.SendReq; @@ -52,10 +53,10 @@ public class MmsSender { } private void handleSendMms(MasterSecret masterSecret, Intent intent) { - long messageId = intent.getLongExtra("message_id", -1); - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); - MmsTransport transport = new MmsTransport(context, masterSecret); + long messageId = intent.getLongExtra("message_id", -1); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); + UniversalTransport transport = new UniversalTransport(context, masterSecret); try { SendReq[] messages = database.getOutgoingMessages(masterSecret, messageId); diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index a030c00b71..9935f393eb 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -5,12 +5,21 @@ import android.util.Log; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; +import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.textsecure.push.PushAttachment; import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.RateLimitException; import org.whispersystems.textsecure.util.PhoneNumberFormatter; import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import ws.com.google.android.mms.ContentType; +import ws.com.google.android.mms.pdu.PduBody; +import ws.com.google.android.mms.pdu.SendReq; public class PushTransport extends BaseTransport { @@ -40,4 +49,37 @@ public class PushTransport extends BaseTransport { throw new IOException("Rate limit exceeded."); } } + + public void deliver(SendReq message, List destinations) throws IOException { + try { + String localNumber = TextSecurePreferences.getLocalNumber(context); + String password = TextSecurePreferences.getPushServerPassword(context); + PushServiceSocket socket = new PushServiceSocket(context, localNumber, password); + String messageText = PartParser.getMessageText(message.getBody()); + List attachments = getAttachmentsFromBody(message.getBody()); + + if (attachments.isEmpty()) socket.sendMessage(destinations, messageText); + else socket.sendMessage(destinations, messageText, attachments); + } catch (RateLimitException e) { + Log.w("PushTransport", e); + throw new IOException("Rate limit exceeded."); + } + } + + private List getAttachmentsFromBody(PduBody body) { + List attachments = new LinkedList(); + + for (int i=0;i deliver(SendReq mediaMessage) throws UndeliverableMessageException { + if (!TextSecurePreferences.isPushRegistered(context)) { + return mmsTransport.deliver(mediaMessage); + } + + List destinations = getMediaDestinations(mediaMessage); + + if (NumberFilter.getInstance(context).containsNumbers(destinations)) { + try { + Log.w("UniversalTransport", "Delivering media message with GCM..."); + pushTransport.deliver(mediaMessage, destinations); + return new Pair("push".getBytes("UTF-8"), 0); + } catch (IOException ioe) { + Log.w("UniversalTransport", ioe); + return mmsTransport.deliver(mediaMessage); + } + } else { + Log.w("UniversalTransport", "Delivering media message with MMS..."); + return mmsTransport.deliver(mediaMessage); + } + } + + private List getMediaDestinations(SendReq mediaMessage) { + LinkedList destinations = new LinkedList(); + + if (mediaMessage.getTo() != null) { + for (EncodedStringValue to : mediaMessage.getTo()) { + destinations.add(Util.canonicalizeNumber(context, to.getString())); + } + } + + if (mediaMessage.getCc() != null) { + for (EncodedStringValue cc : mediaMessage.getCc()) { + destinations.add(Util.canonicalizeNumber(context, cc.getString())); + } + } + + if (mediaMessage.getBcc() != null) { + for (EncodedStringValue bcc : mediaMessage.getBcc()) { + destinations.add(Util.canonicalizeNumber(context, bcc.getString())); + } + } + + return destinations; + } + } diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index b78f56791d..6fad21450a 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -16,6 +16,7 @@ */ package org.thoughtcrime.securesms.util; +import android.content.Context; import android.graphics.Typeface; import android.text.Spannable; import android.text.SpannableString; @@ -26,6 +27,7 @@ import android.os.Build; import android.provider.Telephony; import org.thoughtcrime.securesms.mms.MmsRadio; +import org.whispersystems.textsecure.util.PhoneNumberFormatter; import java.io.UnsupportedEncodingException; import java.util.concurrent.ExecutorService; @@ -144,6 +146,11 @@ public class Util { } } + public static String canonicalizeNumber(Context context, String number) { + String localNumber = TextSecurePreferences.getLocalNumber(context); + return PhoneNumberFormatter.formatNumber(number, localNumber); + } + public static boolean isDefaultSmsProvider(Context context){ return (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) ||