2013-11-10 12:15:29 +00:00
|
|
|
/**
|
|
|
|
* Copyright (C) 2013 Open Whisper Systems
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2013-07-17 02:52:02 +00:00
|
|
|
package org.thoughtcrime.securesms.transport;
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
import android.telephony.TelephonyManager;
|
|
|
|
import android.util.Log;
|
|
|
|
|
2014-11-03 23:16:04 +00:00
|
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
|
|
import org.thoughtcrime.securesms.crypto.MmsCipher;
|
|
|
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
2013-07-17 02:52:02 +00:00
|
|
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
2014-09-16 23:21:41 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
2013-07-17 02:52:02 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.MmsRadio;
|
|
|
|
import org.thoughtcrime.securesms.mms.MmsRadioException;
|
2013-11-19 18:13:24 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.MmsSendResult;
|
2014-09-16 23:21:41 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
|
2014-02-03 03:38:06 +00:00
|
|
|
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
2014-06-13 23:15:33 +00:00
|
|
|
import org.thoughtcrime.securesms.util.NumberUtil;
|
2014-11-03 23:16:04 +00:00
|
|
|
import org.whispersystems.libaxolotl.NoSessionException;
|
2013-08-18 01:37:18 +00:00
|
|
|
import org.whispersystems.textsecure.util.Hex;
|
2013-07-17 02:52:02 +00:00
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
|
|
|
import ws.com.google.android.mms.pdu.EncodedStringValue;
|
|
|
|
import ws.com.google.android.mms.pdu.PduComposer;
|
|
|
|
import ws.com.google.android.mms.pdu.PduHeaders;
|
|
|
|
import ws.com.google.android.mms.pdu.SendConf;
|
|
|
|
import ws.com.google.android.mms.pdu.SendReq;
|
|
|
|
|
|
|
|
public class MmsTransport {
|
|
|
|
|
2014-11-03 23:16:04 +00:00
|
|
|
private static final String TAG = MmsTransport.class.getSimpleName();
|
|
|
|
|
2013-07-17 02:52:02 +00:00
|
|
|
private final Context context;
|
|
|
|
private final MasterSecret masterSecret;
|
|
|
|
private final MmsRadio radio;
|
|
|
|
|
|
|
|
public MmsTransport(Context context, MasterSecret masterSecret) {
|
|
|
|
this.context = context;
|
|
|
|
this.masterSecret = masterSecret;
|
|
|
|
this.radio = MmsRadio.getInstance(context);
|
|
|
|
}
|
|
|
|
|
2014-04-01 01:47:24 +00:00
|
|
|
public MmsSendResult deliver(SendReq message) throws UndeliverableMessageException,
|
|
|
|
InsecureFallbackApprovalException
|
|
|
|
{
|
2014-02-23 22:38:41 +00:00
|
|
|
|
2014-06-13 23:15:33 +00:00
|
|
|
validateDestinations(message);
|
|
|
|
|
2013-07-17 02:52:02 +00:00
|
|
|
try {
|
|
|
|
if (isCdmaDevice()) {
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, "Sending MMS directly without radio change...");
|
2013-07-17 02:52:02 +00:00
|
|
|
try {
|
|
|
|
return sendMms(message, false, false);
|
|
|
|
} catch (IOException e) {
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, e);
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, "Sending MMS with radio change and proxy...");
|
2013-07-17 02:52:02 +00:00
|
|
|
radio.connect();
|
|
|
|
|
|
|
|
try {
|
2014-07-29 22:31:20 +00:00
|
|
|
MmsSendResult result = sendMms(message, true, true);
|
2013-07-17 02:52:02 +00:00
|
|
|
radio.disconnect();
|
|
|
|
return result;
|
|
|
|
} catch (IOException e) {
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, e);
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|
|
|
|
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, "Sending MMS with radio change and without proxy...");
|
2013-07-17 02:52:02 +00:00
|
|
|
|
|
|
|
try {
|
2014-07-29 22:31:20 +00:00
|
|
|
MmsSendResult result = sendMms(message, true, false);
|
2013-07-17 02:52:02 +00:00
|
|
|
radio.disconnect();
|
|
|
|
return result;
|
|
|
|
} catch (IOException ioe) {
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, ioe);
|
2013-07-17 02:52:02 +00:00
|
|
|
radio.disconnect();
|
|
|
|
throw new UndeliverableMessageException(ioe);
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (MmsRadioException mre) {
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, mre);
|
2013-07-17 02:52:02 +00:00
|
|
|
throw new UndeliverableMessageException(mre);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-11-19 18:13:24 +00:00
|
|
|
private MmsSendResult sendMms(SendReq message, boolean usingMmsRadio, boolean useProxy)
|
2014-04-01 01:47:24 +00:00
|
|
|
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
|
2013-07-17 02:52:02 +00:00
|
|
|
{
|
2013-11-19 18:13:24 +00:00
|
|
|
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
|
|
|
|
boolean upgradedSecure = false;
|
2013-07-17 02:52:02 +00:00
|
|
|
|
|
|
|
if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) {
|
2013-11-19 18:13:24 +00:00
|
|
|
message = getEncryptedMessage(message);
|
|
|
|
upgradedSecure = true;
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (number != null && number.trim().length() != 0) {
|
|
|
|
message.setFrom(new EncodedStringValue(number));
|
|
|
|
}
|
|
|
|
|
2014-09-16 23:21:41 +00:00
|
|
|
try {
|
|
|
|
OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), new PduComposer(context, message).make());
|
|
|
|
SendConf conf = connection.send(usingMmsRadio, useProxy);
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2014-09-16 23:21:41 +00:00
|
|
|
for (int i=0;i<message.getBody().getPartsNum();i++) {
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, "Sent MMS part of content-type: " + new String(message.getBody().getPart(i).getContentType()));
|
2014-09-16 23:21:41 +00:00
|
|
|
}
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2014-09-16 23:21:41 +00:00
|
|
|
if (conf == null) {
|
|
|
|
throw new UndeliverableMessageException("No M-Send.conf received in response to send.");
|
|
|
|
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
|
|
|
|
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus());
|
|
|
|
} else if (isInconsistentResponse(message, conf)) {
|
|
|
|
throw new UndeliverableMessageException("Mismatched response!");
|
|
|
|
} else {
|
|
|
|
return new MmsSendResult(conf.getMessageId(), conf.getResponseStatus(), upgradedSecure, false);
|
|
|
|
}
|
|
|
|
} catch (ApnUnavailableException aue) {
|
|
|
|
throw new IOException("no APN was retrievable");
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-04-01 01:47:24 +00:00
|
|
|
private SendReq getEncryptedMessage(SendReq pdu) throws InsecureFallbackApprovalException {
|
2014-02-03 03:38:06 +00:00
|
|
|
try {
|
2014-11-03 23:16:04 +00:00
|
|
|
MmsCipher cipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret));
|
|
|
|
return cipher.encrypt(context, pdu);
|
|
|
|
} catch (NoSessionException e) {
|
|
|
|
throw new InsecureFallbackApprovalException(e);
|
2014-02-03 03:38:06 +00:00
|
|
|
} catch (RecipientFormattingException e) {
|
|
|
|
throw new AssertionError(e);
|
|
|
|
}
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private boolean isInconsistentResponse(SendReq message, SendConf response) {
|
2014-11-03 23:16:04 +00:00
|
|
|
Log.w(TAG, "Comparing: " + Hex.toString(message.getTransactionId()));
|
|
|
|
Log.w(TAG, "With: " + Hex.toString(response.getTransactionId()));
|
2013-07-17 02:52:02 +00:00
|
|
|
return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean isCdmaDevice() {
|
|
|
|
return ((TelephonyManager)context
|
|
|
|
.getSystemService(Context.TELEPHONY_SERVICE))
|
|
|
|
.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
|
|
|
|
}
|
|
|
|
|
2014-06-13 23:15:33 +00:00
|
|
|
private void validateDestination(EncodedStringValue destination) throws UndeliverableMessageException {
|
|
|
|
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
|
|
|
|
throw new UndeliverableMessageException("Invalid destination: " +
|
|
|
|
(destination == null ? null : destination.getString()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void validateDestinations(SendReq message) throws UndeliverableMessageException {
|
|
|
|
if (message.getTo() != null) {
|
|
|
|
for (EncodedStringValue to : message.getTo()) {
|
|
|
|
validateDestination(to);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.getCc() != null) {
|
|
|
|
for (EncodedStringValue cc : message.getCc()) {
|
|
|
|
validateDestination(cc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.getBcc() != null) {
|
|
|
|
for (EncodedStringValue bcc : message.getBcc()) {
|
|
|
|
validateDestination(bcc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
|
|
|
|
throw new UndeliverableMessageException("No to, cc, or bcc specified!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|