From 89fb80fcc5e4c35169840877d98c599006f9628d Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Tue, 16 Sep 2014 16:21:41 -0700 Subject: [PATCH] MmsConnection refactor - Use Apache HttpClient v4.x, only library that seems to like HTTP proxies - Remove custom redirect logic in favor of library's Fixes #1904 // FREEBIE --- build.gradle | 2 + .../securesms/ConversationActivity.java | 4 +- .../securesms/MmsPreferencesActivity.java | 6 +- .../securesms/database/ApnDatabase.java | 50 +++- .../securesms/mms/IncomingMmsConnection.java | 93 +++++++ .../securesms/mms/MmsCommunication.java | 254 ------------------ .../securesms/mms/MmsConnection.java | 189 +++++++++++++ .../securesms/mms/MmsDownloadHelper.java | 130 --------- .../securesms/mms/MmsSendHelper.java | 163 ----------- .../securesms/mms/OutgoingMmsConnection.java | 111 ++++++++ .../securesms/service/MmsDownloader.java | 23 +- .../securesms/transport/MmsTransport.java | 34 +-- 12 files changed, 470 insertions(+), 589 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java delete mode 100644 src/org/thoughtcrime/securesms/mms/MmsCommunication.java create mode 100644 src/org/thoughtcrime/securesms/mms/MmsConnection.java delete mode 100644 src/org/thoughtcrime/securesms/mms/MmsDownloadHelper.java delete mode 100644 src/org/thoughtcrime/securesms/mms/MmsSendHelper.java create mode 100644 src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java diff --git a/build.gradle b/build.gradle index 476034b8f5..c715208048 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,7 @@ dependencies { compile 'com.google.android.gms:play-services:5.0.89' compile 'com.astuetz:pagerslidingtabstrip:1.0.1' compile 'org.w3c:smil:1.0.0' + compile 'org.apache.httpcomponents:httpclient-android:4.3.5' androidTestCompile 'com.squareup:fest-android:1.0.8' @@ -49,6 +50,7 @@ dependencyVerification { 'com.google.android.gms:play-services:38f326e525830f1d70f60f594ceafcbdf5b312287ddbecd338fd1ed7958a4b1e', 'com.astuetz:pagerslidingtabstrip:f1641396732c7132a7abb837e482e5ee2b0ebb8d10813fc52bbaec2c15c184c2', 'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b', + 'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1', 'com.google.protobuf:protobuf-java:ad9769a22989e688a46af4d3accc348cc501ced22118033230542bc916e33f0b', 'com.madgag:sc-light-jdk15on:931f39d351429fb96c2f749e7ecb1a256a8ebbf5edca7995c9cc085b94d1841d', 'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab', diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 4b6a6c386c..d73f4d43d8 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -73,9 +73,9 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter; import org.thoughtcrime.securesms.mms.MediaTooLargeException; -import org.thoughtcrime.securesms.mms.MmsSendHelper; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.thoughtcrime.securesms.mms.OutgoingMmsConnection; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -736,7 +736,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi new AsyncTask() { @Override protected Boolean doInBackground(Void... params) { - return MmsSendHelper.hasNecessaryApnDetails(ConversationActivity.this); + return OutgoingMmsConnection.isConnectionPossible(ConversationActivity.this); } @Override diff --git a/src/org/thoughtcrime/securesms/MmsPreferencesActivity.java b/src/org/thoughtcrime/securesms/MmsPreferencesActivity.java index 47ff8bc5f0..d6f5c9b336 100644 --- a/src/org/thoughtcrime/securesms/MmsPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/MmsPreferencesActivity.java @@ -20,11 +20,11 @@ import android.content.Intent; import android.os.Bundle; import android.preference.EditTextPreference; import android.preference.Preference; -import android.preference.PreferenceManager; import android.widget.Toast; import com.actionbarsherlock.view.MenuItem; -import org.thoughtcrime.securesms.mms.MmsDownloadHelper; + +import org.thoughtcrime.securesms.mms.IncomingMmsConnection; import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -86,7 +86,7 @@ public class MmsPreferencesActivity extends PassphraseRequiredSherlockPreference } private void initializePreferences() { - if (!MmsDownloadHelper.isMmsConnectionParametersAvailable(this, null)) { + if (!IncomingMmsConnection.isConnectionPossible(this, null)) { TextSecurePreferences.setUseLocalApnsEnabled(this, true); addPreferencesFromResource(R.xml.mms_preferences); this.findPreference(TextSecurePreferences.ENABLE_MANUAL_MMS_PREF).setOnPreferenceChangeListener(new OverrideMmsChangeListener()); diff --git a/src/org/thoughtcrime/securesms/database/ApnDatabase.java b/src/org/thoughtcrime/securesms/database/ApnDatabase.java index c839be415a..fe4c50d40c 100644 --- a/src/org/thoughtcrime/securesms/database/ApnDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ApnDatabase.java @@ -20,17 +20,16 @@ import android.content.Context; import android.content.res.AssetManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; -import org.thoughtcrime.securesms.mms.MmsCommunication; -import org.thoughtcrime.securesms.mms.MmsCommunication.MmsConnectionParameters; +import org.thoughtcrime.securesms.mms.ApnUnavailableException; +import org.thoughtcrime.securesms.mms.MmsConnection.Apn; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.util.Util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.security.InvalidParameterException; /** * Database to query APN and MMSC information @@ -39,6 +38,7 @@ public class ApnDatabase { private static final String TAG = ApnDatabase.class.getSimpleName(); private final SQLiteDatabase db; + private final Context context; private static final String DATABASE_NAME = "apns.db"; private static final String ASSET_PATH = "databases" + File.separator + DATABASE_NAME; @@ -77,6 +77,8 @@ public class ApnDatabase { } private ApnDatabase(final Context context) throws IOException { + this.context = context; + File dbFile = context.getDatabasePath(DATABASE_NAME); if (!dbFile.getParentFile().exists() && !dbFile.getParentFile().mkdir()) { @@ -90,10 +92,36 @@ public class ApnDatabase { null, SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS); } + protected Apn getLocallyConfiguredMmsConnectionParameters() throws ApnUnavailableException { + if (TextSecurePreferences.isUseLocalApnsEnabled(context)) { + String mmsc = TextSecurePreferences.getMmscUrl(context); + + if (mmsc == null) + throw new ApnUnavailableException("Malformed locally configured MMSC."); + + if (!mmsc.startsWith("http")) + mmsc = "http://" + mmsc; + + String proxy = TextSecurePreferences.getMmscProxy(context); + String port = TextSecurePreferences.getMmscProxyPort(context); + + return new Apn(mmsc, proxy, port); + } + + throw new ApnUnavailableException("No locally configured parameters available"); + + } - public MmsCommunication.MmsConnectionParameters getMmsConnectionParameters(final String mccmnc, - final String apn) - { + public Apn getMmsConnectionParameters(final String mccmnc, final String apn) { + + if (TextSecurePreferences.isUseLocalApnsEnabled(context)) { + Log.w(TAG, "Choosing locally-overridden MMS settings"); + try { + return getLocallyConfiguredMmsConnectionParameters(); + } catch (ApnUnavailableException aue) { + Log.w(TAG, "preference to use local apn set, but no parameters avaiable. falling back."); + } + } if (mccmnc == null) { Log.w(TAG, "mccmnc was null, returning null"); @@ -121,10 +149,10 @@ public class ApnDatabase { } if (cursor != null && cursor.moveToFirst()) { - MmsConnectionParameters params = new MmsConnectionParameters(cursor.getString(cursor.getColumnIndexOrThrow(MMSC_COLUMN)), - cursor.getString(cursor.getColumnIndexOrThrow(MMS_PROXY_COLUMN)), - cursor.getString(cursor.getColumnIndexOrThrow(MMS_PORT_COLUMN))); - Log.w(TAG, "Returning preferred APN " + params.get().get(0)); + Apn params = new Apn(cursor.getString(cursor.getColumnIndexOrThrow(MMSC_COLUMN)), + cursor.getString(cursor.getColumnIndexOrThrow(MMS_PROXY_COLUMN)), + cursor.getString(cursor.getColumnIndexOrThrow(MMS_PORT_COLUMN))); + Log.w(TAG, "Returning preferred APN " + params); return params; } diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java b/src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java new file mode 100644 index 0000000000..d29a9c8f69 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2014 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 . + */ +package org.thoughtcrime.securesms.mms; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGetHC4; +import org.apache.http.client.methods.HttpUriRequest; + +import java.io.IOException; +import java.util.Arrays; + +import ws.com.google.android.mms.pdu.PduParser; +import ws.com.google.android.mms.pdu.RetrieveConf; + +public class IncomingMmsConnection extends MmsConnection { + private static final String TAG = IncomingMmsConnection.class.getSimpleName(); + + public IncomingMmsConnection(Context context, Apn apn) { + super(context, apn); + } + + @Override + protected HttpUriRequest constructRequest(boolean useProxy) throws IOException { + HttpGetHC4 request = new HttpGetHC4(apn.getMmsc()); + request.addHeader("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"); + if (useProxy) { + HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort()); + request.setConfig(RequestConfig.custom().setProxy(proxy).build()); + } + return request; + } + + public static boolean isConnectionPossible(Context context, String apn) { + try { + getApn(context, apn); + return true; + } catch (ApnUnavailableException e) { + return false; + } + } + + public RetrieveConf retrieve(boolean usingMmsRadio, boolean useProxyIfAvailable) + throws IOException, ApnUnavailableException + { + byte[] pdu = null; + + final boolean useProxy = useProxyIfAvailable && apn.hasProxy(); + final String targetHost = useProxy + ? apn.getProxy() + : Uri.parse(apn.getMmsc()).getHost(); + try { + if (checkRouteToHost(context, targetHost, usingMmsRadio)) { + Log.w(TAG, "got successful route to host " + targetHost); + pdu = makeRequest(useProxy); + } + } catch (IOException ioe) { + Log.w(TAG, ioe); + } + + if (pdu == null) { + throw new IOException("Connection manager could not obtain route to host."); + } + + RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse(); + + if (retrieved == null) { + Log.w(TAG, "Couldn't parse PDU, byte response: " + Arrays.toString(pdu)); + Log.w(TAG, "Couldn't parse PDU, ASCII: " + new String(pdu)); + throw new IOException("Bad retrieved PDU"); + } + + return retrieved; + } +} diff --git a/src/org/thoughtcrime/securesms/mms/MmsCommunication.java b/src/org/thoughtcrime/securesms/mms/MmsCommunication.java deleted file mode 100644 index bbe385f359..0000000000 --- a/src/org/thoughtcrime/securesms/mms/MmsCommunication.java +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Copyright (C) 2011 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 . - */ -package org.thoughtcrime.securesms.mms; - -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteException; -import android.net.ConnectivityManager; -import android.util.Log; - -import org.thoughtcrime.securesms.database.ApnDatabase; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.util.TelephonyUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.textsecure.util.Conversions; -import org.whispersystems.textsecure.util.Util; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -public class MmsCommunication { - private static final String TAG = "MmsCommunication"; - - public static final int MAX_REDIRECTS = 10; - - protected static MmsConnectionParameters getLocallyConfiguredMmsConnectionParameters(Context context) - throws ApnUnavailableException - { - if (TextSecurePreferences.isUseLocalApnsEnabled(context)) { - String mmsc = TextSecurePreferences.getMmscUrl(context); - - if (mmsc == null) - throw new ApnUnavailableException("Malformed locally configured MMSC."); - - if (!mmsc.startsWith("http")) - mmsc = "http://" + mmsc; - - String proxy = TextSecurePreferences.getMmscProxy(context); - String port = TextSecurePreferences.getMmscProxyPort(context); - - return new MmsConnectionParameters(mmsc, proxy, port); - } - - throw new ApnUnavailableException("No locally configured parameters available"); - } - - protected static MmsConnectionParameters getLocalMmsConnectionParameters(Context context) - throws ApnUnavailableException - { - if (TextSecurePreferences.isUseLocalApnsEnabled(context)) { - return getLocallyConfiguredMmsConnectionParameters(context); - } else { - try { - MmsConnectionParameters params = ApnDatabase.getInstance(context) - .getMmsConnectionParameters(TelephonyUtil.getMccMnc(context), - TelephonyUtil.getApn(context)); - - if (params == null) { - throw new ApnUnavailableException("No parameters available from ApnDefaults."); - } - - return params; - } catch (IOException ioe) { - throw new ApnUnavailableException("ApnDatabase threw an IOException", ioe); - } - } - } - - protected static MmsConnectionParameters getMmsConnectionParameters(Context context, String apn) - throws ApnUnavailableException - { - Log.w(TAG, "Getting MMSC params for apn " + apn); - Cursor cursor = null; - - try { - cursor = DatabaseFactory.getMmsDatabase(context).getCarrierMmsInformation(apn); - - if (cursor == null || !cursor.moveToFirst()) { - Log.w(TAG, "Android didn't have a result, querying local parameters."); - return getLocalMmsConnectionParameters(context); - } - - do { - String mmsc = cursor.getString(cursor.getColumnIndexOrThrow("mmsc")); - String proxy = cursor.getString(cursor.getColumnIndexOrThrow("mmsproxy")); - String port = cursor.getString(cursor.getColumnIndexOrThrow("mmsport")); - - if (!Util.isEmpty(mmsc)) { - Log.w(TAG, "Using Android-provided MMSC parameters."); - return new MmsConnectionParameters(mmsc, proxy, port); - } - - } while (cursor.moveToNext()); - - Log.w(TAG, "Android provided results were empty, querying local parameters."); - return getLocalMmsConnectionParameters(context); - } catch (SQLiteException sqe) { - Log.w(TAG, sqe); - } catch (SecurityException se) { - Log.w(TAG, "Android won't let us query the APN database."); - return getLocalMmsConnectionParameters(context); - } catch (IllegalArgumentException iae) { - Log.w(TAG, iae); - } finally { - if (cursor != null) - cursor.close(); - } - return getLocalMmsConnectionParameters(context); - } - - protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio) - throws IOException - { - InetAddress inetAddress = InetAddress.getByName(host); - - if (!usingMmsRadio) { - if (inetAddress.isSiteLocalAddress()) { - throw new IOException("RFC1918 address in non-MMS radio situation!"); - } - - return true; - } - - Log.w(TAG, "Checking route to address: " + host + " , " + inetAddress.getHostAddress()); - - byte[] ipAddressBytes = inetAddress.getAddress(); - - if (ipAddressBytes != null && ipAddressBytes.length == 4) { - int ipAddress = Conversions.byteArrayToIntLittleEndian(ipAddressBytes, 0); - ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - - return manager.requestRouteToHost(MmsRadio.TYPE_MOBILE_MMS, ipAddress); - } - return true; - } - - protected static byte[] parseResponse(InputStream is) throws IOException { - InputStream in = new BufferedInputStream(is); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Util.copy(in, baos); - - Log.w(TAG, "Received full server response, " + baos.size() + " bytes"); - - return baos.toByteArray(); - } - - protected static HttpURLConnection constructHttpClient(String urlString, String proxy, int port) - throws IOException - { - HttpURLConnection urlConnection; - URL url = new URL(urlString); - - if (proxy != null) { - Log.w(TAG, String.format("Constructing http client using a proxy: (%s:%d)", proxy, port)); - Proxy proxyRoute = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxy, port)); - urlConnection = (HttpURLConnection) url.openConnection(proxyRoute); - } else { - Log.w(TAG, "Constructing http client without proxy"); - urlConnection = (HttpURLConnection) url.openConnection(); - } - - urlConnection.setInstanceFollowRedirects(false); - urlConnection.setConnectTimeout(20*1000); - urlConnection.setReadTimeout(20*1000); - urlConnection.setUseCaches(false); - urlConnection.setRequestProperty("User-Agent", "Android-Mms/2.0"); - urlConnection.setRequestProperty("Accept-Charset", "UTF-8"); - return urlConnection; - } - - public static class MmsConnectionParameters { - - public class Apn { - private final String mmsc; - private final String proxy; - private final String port; - - public Apn(String mmsc, String proxy, String port) { - this.mmsc = mmsc; - this.proxy = proxy; - this.port = port; - } - - public boolean hasProxy() { - return !Util.isEmpty(proxy); - } - - public String getMmsc() { - return mmsc; - } - - public String getProxy() { - if (!hasProxy()) - return null; - - return proxy; - } - - public int getPort() { - if (Util.isEmpty(port)) - return 80; - - return Integer.parseInt(port); - } - - @Override - public String toString() { - return MmsConnectionParameters.class.getSimpleName() + - "{ mmsc: \"" + mmsc + "\"" + - ", proxy: " + (proxy == null ? "none" : '"' + proxy + '"') + - ", port: " + (port == null ? "none" : port) + " }"; - } - } - - private List apn = new ArrayList(); - - public MmsConnectionParameters(String mmsc, String proxy, String port) { - apn.add(new Apn(mmsc, proxy, port)); - } - - public MmsConnectionParameters add(String mmsc, String proxy, String port) { - apn.add(new Apn(mmsc, proxy, port)); - return this; - } - - public List get() { - return apn; - } - } - -} diff --git a/src/org/thoughtcrime/securesms/mms/MmsConnection.java b/src/org/thoughtcrime/securesms/mms/MmsConnection.java new file mode 100644 index 0000000000..632f24a7ec --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/MmsConnection.java @@ -0,0 +1,189 @@ +/** + * Copyright (C) 2011 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 . + */ +package org.thoughtcrime.securesms.mms; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.text.TextUtils; +import android.util.Log; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.NoConnectionReuseStrategyHC4; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.LaxRedirectStrategy; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.thoughtcrime.securesms.database.ApnDatabase; +import org.thoughtcrime.securesms.util.TelephonyUtil; +import org.whispersystems.textsecure.util.Conversions; +import org.whispersystems.textsecure.util.Util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; + +public abstract class MmsConnection { + private static final String TAG = "MmsCommunication"; + + protected final Context context; + protected final Apn apn; + + protected MmsConnection(Context context, Apn apn) { + this.context = context; + this.apn = apn; + } + + protected static Apn getLocalApn(Context context) throws ApnUnavailableException { + try { + Apn params = ApnDatabase.getInstance(context) + .getMmsConnectionParameters(TelephonyUtil.getMccMnc(context), + TelephonyUtil.getApn(context)); + + if (params == null) { + throw new ApnUnavailableException("No parameters available from ApnDefaults."); + } + + return params; + } catch (IOException ioe) { + throw new ApnUnavailableException("ApnDatabase threw an IOException", ioe); + } + } + + public static Apn getApn(Context context, String apnName) throws ApnUnavailableException { + Log.w(TAG, "Getting MMSC params for apn " + apnName); + return getLocalApn(context); + } + + protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio) + throws IOException + { + InetAddress inetAddress = InetAddress.getByName(host); + if (!usingMmsRadio) { + if (inetAddress.isSiteLocalAddress()) { + throw new IOException("RFC1918 address in non-MMS radio situation!"); + } + Log.w(TAG, "returning vacuous success since MMS radio is not in use"); + return true; + } + byte[] ipAddressBytes = inetAddress.getAddress(); + if (ipAddressBytes == null || ipAddressBytes.length != 4) { + Log.w(TAG, "returning vacuous success since android.net package doesn't support IPv6"); + return true; + } + + Log.w(TAG, "Checking route to address: " + host + ", " + inetAddress.getHostAddress()); + ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + int ipAddress = Conversions.byteArrayToIntLittleEndian(ipAddressBytes, 0); + boolean routeToHostObtained = manager.requestRouteToHost(MmsRadio.TYPE_MOBILE_MMS, ipAddress); + Log.w(TAG, "requestRouteToHost result: " + routeToHostObtained); + return routeToHostObtained; + } + + protected static byte[] parseResponse(InputStream is) throws IOException { + InputStream in = new BufferedInputStream(is); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.copy(in, baos); + + Log.w(TAG, "Received full server response, " + baos.size() + " bytes"); + + return baos.toByteArray(); + } + + protected CloseableHttpClient constructHttpClient() + throws IOException { + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(20 * 1000) + .setConnectionRequestTimeout(20 * 1000) + .setSocketTimeout(20 * 1000) + .setMaxRedirects(20) + .build(); + + return HttpClients.custom() + .setConnectionReuseStrategy(new NoConnectionReuseStrategyHC4()) + .setRedirectStrategy(new LaxRedirectStrategy()) + .setUserAgent("Android-Mms/2.0") + .setConnectionManager(new BasicHttpClientConnectionManager()) + .setDefaultRequestConfig(config) + .build(); + } + + protected byte[] makeRequest(boolean useProxy) throws IOException { + Log.w(TAG, "connecting to " + apn.getMmsc() + (useProxy ? " using proxy" : "")); + + HttpUriRequest request; + CloseableHttpClient client = null; + CloseableHttpResponse response = null; + try { + request = constructRequest(useProxy); + client = constructHttpClient(); + response = client.execute(request); + + Log.w(TAG, "* response code: " + response.getStatusLine()); + + if (response.getStatusLine().getStatusCode() == 200) { + return parseResponse(response.getEntity().getContent()); + } + } finally { + if (response != null) response.close(); + if (client != null) client.close(); + } + + throw new IOException("unhandled response code"); + } + + protected abstract HttpUriRequest constructRequest(boolean useProxy) throws IOException; + + public static class Apn { + private final String mmsc; + private final String proxy; + private final String port; + + public Apn(String mmsc, String proxy, String port) { + this.mmsc = mmsc; + this.proxy = proxy; + this.port = port; + } + + public boolean hasProxy() { + return !TextUtils.isEmpty(proxy); + } + + public String getMmsc() { + return mmsc; + } + + public String getProxy() { + return hasProxy() ? proxy : null; + } + + public int getPort() { + return TextUtils.isEmpty(port) ? 80 : Integer.parseInt(port); + } + + @Override + public String toString() { + return Apn.class.getSimpleName() + + "{ mmsc: \"" + mmsc + "\"" + + ", proxy: " + (proxy == null ? "none" : '"' + proxy + '"') + + ", port: " + (port == null ? "none" : port) + " }"; + } + } +} diff --git a/src/org/thoughtcrime/securesms/mms/MmsDownloadHelper.java b/src/org/thoughtcrime/securesms/mms/MmsDownloadHelper.java deleted file mode 100644 index b48f7e1cc6..0000000000 --- a/src/org/thoughtcrime/securesms/mms/MmsDownloadHelper.java +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright (C) 2011 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 . - */ -package org.thoughtcrime.securesms.mms; - -import android.content.Context; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import ws.com.google.android.mms.pdu.PduParser; -import ws.com.google.android.mms.pdu.RetrieveConf; - -public class MmsDownloadHelper extends MmsCommunication { - private static final String TAG = MmsDownloadHelper.class.getSimpleName(); - - private static byte[] makeRequest(String url, String proxy, int proxyPort) - throws IOException - { - HttpURLConnection client = null; - - int redirects = MAX_REDIRECTS; - final Set previousUrls = new HashSet(); - String currentUrl = url; - while (redirects-- > 0) { - if (previousUrls.contains(currentUrl)) { - throw new IOException("redirect loop detected"); - } - try { - client = constructHttpClient(currentUrl, proxy, proxyPort); - - client.setDoInput(true); - client.setRequestMethod("GET"); - client.setRequestProperty("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"); - - Log.w(TAG, "connecting to " + currentUrl); - client.connect(); - - int responseCode = client.getResponseCode(); - Log.w(TAG, "* response code: " + responseCode + "/" + client.getResponseMessage()); - - if (responseCode == 301 || responseCode == 302) { - final String redirectUrl = client.getHeaderField("Location"); - Log.w(TAG, "* Location: " + redirectUrl); - if (TextUtils.isEmpty(redirectUrl)) { - throw new IOException("Got redirect response code, but Location header was empty or missing"); - } - previousUrls.add(currentUrl); - currentUrl = redirectUrl; - } else if (responseCode == 200) { - final InputStream is = client.getInputStream(); - return parseResponse(is); - } else { - throw new IOException("unhandled response code"); - } - } finally { - if (client != null) client.disconnect(); - } - } - throw new IOException("max redirects hit"); - } - - public static boolean isMmsConnectionParametersAvailable(Context context, String apn) { - try { - getMmsConnectionParameters(context, apn); - return true; - } catch (ApnUnavailableException e) { - return false; - } - } - - public static RetrieveConf retrieveMms(Context context, String url, String apn, - boolean usingMmsRadio, boolean proxyIfPossible) - throws IOException, ApnUnavailableException - { - MmsConnectionParameters connectionParameters = getMmsConnectionParameters(context, apn); - byte[] pdu = null; - - for (MmsConnectionParameters.Apn param : connectionParameters.get()) { - try { - if (proxyIfPossible && param.hasProxy()) { - if (checkRouteToHost(context, param.getProxy(), usingMmsRadio)) { - pdu = makeRequest(url, param.getProxy(), param.getPort()); - } - } else { - if (checkRouteToHost(context, Uri.parse(url).getHost(), usingMmsRadio)) { - pdu = makeRequest(url, null, -1); - } - } - - if (pdu != null) break; - } catch (IOException ioe) { - Log.w(TAG, ioe); - } - } - - if (pdu == null) { - throw new IOException("Connection manager could not obtain route to host."); - } - - RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse(); - - if (retrieved == null) { - Log.w(TAG, "Couldn't parse PDU, raw server response: " + Arrays.toString(pdu)); - throw new IOException("Bad retrieved PDU"); - } - - return retrieved; - } -} diff --git a/src/org/thoughtcrime/securesms/mms/MmsSendHelper.java b/src/org/thoughtcrime/securesms/mms/MmsSendHelper.java deleted file mode 100644 index d1391483fb..0000000000 --- a/src/org/thoughtcrime/securesms/mms/MmsSendHelper.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (C) 2011 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 . - */ -package org.thoughtcrime.securesms.mms; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.util.HashSet; -import java.util.Set; - -import ws.com.google.android.mms.pdu.PduParser; -import ws.com.google.android.mms.pdu.SendConf; - -public class MmsSendHelper extends MmsCommunication { - private final static String TAG = MmsSendHelper.class.getSimpleName(); - - private static byte[] makePost(String url, String proxy, int proxyPort, byte[] mms) - throws IOException - { - if (mms == null) return null; - - HttpURLConnection client = null; - - int redirects = MAX_REDIRECTS; - final Set previousUrls = new HashSet(); - String currentUrl = url; - while (redirects-- > 0) { - if (previousUrls.contains(currentUrl)) { - throw new IOException("redirect loop detected"); - } - try { - client = constructHttpClient(currentUrl, proxy, proxyPort); - client.setFixedLengthStreamingMode(mms.length); - client.setDoInput(true); - client.setDoOutput(true); - client.setRequestMethod("POST"); - client.setRequestProperty("Content-Type", "application/vnd.wap.mms-message"); - client.setRequestProperty("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"); - client.setRequestProperty("x-wap-profile", "http://www.google.com/oha/rdf/ua-profile-kila.xml"); - - Log.w(TAG, "connecting to " + currentUrl); - client.connect(); - - Log.w(TAG, "* writing mms payload, " + mms.length + " bytes"); - OutputStream out = client.getOutputStream(); - out.write(mms); - out.flush(); - out.close(); - - Log.w(TAG, "* payload sent"); - - int responseCode = client.getResponseCode(); - Log.w(TAG, "* response code: " + responseCode + "/" + client.getResponseMessage()); - - if (responseCode == 301 || responseCode == 302) { - final String redirectUrl = client.getHeaderField("Location"); - Log.w(TAG, "* Location: " + redirectUrl); - if (TextUtils.isEmpty(redirectUrl)) { - throw new IOException("Got redirect response code, but Location header was empty or missing"); - } - previousUrls.add(currentUrl); - currentUrl = redirectUrl; - } else if (responseCode == 200) { - final InputStream is = client.getInputStream(); - return parseResponse(is); - } else { - throw new IOException("unhandled response code"); - } - } finally { - if (client != null) client.disconnect(); - } - } - throw new IOException("max redirects hit"); - } - - public static void sendNotificationReceived(Context context, byte[] mms, String apn, - boolean usingMmsRadio, boolean useProxyIfAvailable) - throws IOException - { - sendBytes(context, mms, apn, usingMmsRadio, useProxyIfAvailable); - } - - public static SendConf sendMms(Context context, byte[] mms, String apn, - boolean usingMmsRadio, boolean useProxyIfAvailable) - throws IOException - { - byte[] response = sendBytes(context, mms, apn, usingMmsRadio, useProxyIfAvailable); - return (SendConf) new PduParser(response).parse(); - } - - private static byte[] sendBytes(Context context, byte[] mms, String apn, - boolean usingMmsRadio, boolean useProxyIfAvailable) - throws IOException - { - Log.w(TAG, "Sending MMS of length: " + mms.length + "." + (usingMmsRadio ? " using mms radio" : "")); - try { - MmsConnectionParameters parameters = getMmsConnectionParameters(context, apn); - - for (MmsConnectionParameters.Apn param : parameters.get()) { - try { - if (useProxyIfAvailable && param.hasProxy()) { - if (checkRouteToHost(context, param.getProxy(), usingMmsRadio)) { - byte[] response = makePost(param.getMmsc(), param.getProxy(), param.getPort(), mms); - if (response != null) return response; - } - } else { - if (checkRouteToHost(context, Uri.parse(param.getMmsc()).getHost(), usingMmsRadio)) { - byte[] response = makePost(param.getMmsc(), null, -1, mms); - if (response != null) return response; - } - } - } catch (IOException ioe) { - Log.w(TAG, ioe); - } - } - - throw new IOException("Connection manager could not obtain route to host."); - } catch (ApnUnavailableException aue) { - Log.w(TAG, aue); - throw new IOException("Failed to get MMSC information..."); - } - } - - public static boolean hasNecessaryApnDetails(Context context) { - try { - ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS); - if (networkInfo == null) { - Log.w(TAG, "MMS network info was null, unsupported by this device"); - return false; - } - String apn = networkInfo.getExtraInfo(); - - MmsCommunication.getMmsConnectionParameters(context, apn); - return true; - } catch (ApnUnavailableException e) { - Log.w("MmsSendHelper", e); - return false; - } - } -} diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java b/src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java new file mode 100644 index 0000000000..b509dceee3 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2014 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 . + */ +package org.thoughtcrime.securesms.mms; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.util.Log; + +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPostHC4; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ByteArrayEntityHC4; + +import java.io.IOException; + +import ws.com.google.android.mms.pdu.PduParser; +import ws.com.google.android.mms.pdu.SendConf; + +public class OutgoingMmsConnection extends MmsConnection { + private final static String TAG = OutgoingMmsConnection.class.getSimpleName(); + + private final byte[] mms; + + public OutgoingMmsConnection(Context context, String apnName, byte[] mms) throws ApnUnavailableException { + super(context, getApn(context, apnName)); + this.mms = mms; + } + + @Override + protected HttpUriRequest constructRequest(boolean useProxy) + throws IOException + { + HttpPostHC4 request = new HttpPostHC4(apn.getMmsc()); + request.addHeader("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"); + request.addHeader("x-wap-profile", "http://www.google.com/oha/rdf/ua-profile-kila.xml"); + request.addHeader("Content-Type", "application/vnd.wap.mms-message"); + request.setEntity(new ByteArrayEntityHC4(mms)); + if (useProxy) { + HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort()); + request.setConfig(RequestConfig.custom().setProxy(proxy).build()); + } + return request; + } + + public void sendNotificationReceived(boolean usingMmsRadio, boolean useProxyIfAvailable) + throws IOException + { + sendBytes(usingMmsRadio, useProxyIfAvailable); + } + + public SendConf send(boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException { + byte[] response = sendBytes(useMmsRadio, useProxyIfAvailable); + return (SendConf) new PduParser(response).parse(); + } + + private byte[] sendBytes(boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException { + final boolean useProxy = useProxyIfAvailable && apn.hasProxy(); + final String targetHost = useProxy + ? apn.getProxy() + : Uri.parse(apn.getMmsc()).getHost(); + + Log.w(TAG, "Sending MMS of length: " + mms.length + + (useMmsRadio ? ", using mms radio" : "") + + (useProxy ? ", using proxy" : "")); + + try { + if (checkRouteToHost(context, targetHost, useMmsRadio)) { + Log.w(TAG, "got successful route to host " + targetHost); + byte[] response = makeRequest(useProxy); + if (response != null) return response; + } + } catch (IOException ioe) { + Log.w(TAG, ioe); + } + throw new IOException("Connection manager could not obtain route to host."); + } + + public static boolean isConnectionPossible(Context context) { + try { + ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS); + if (networkInfo == null) { + Log.w(TAG, "MMS network info was null, unsupported by this device"); + return false; + } + + getApn(context, networkInfo.getExtraInfo()); + return true; + } catch (ApnUnavailableException e) { + Log.w(TAG, e); + return false; + } + } +} diff --git a/src/org/thoughtcrime/securesms/service/MmsDownloader.java b/src/org/thoughtcrime/securesms/service/MmsDownloader.java index c957c49f7b..e4a812a29d 100644 --- a/src/org/thoughtcrime/securesms/service/MmsDownloader.java +++ b/src/org/thoughtcrime/securesms/service/MmsDownloader.java @@ -25,22 +25,22 @@ import android.util.Pair; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.DecryptingQueue; +import org.thoughtcrime.securesms.mms.IncomingMmsConnection; +import org.thoughtcrime.securesms.mms.MmsConnection; +import org.thoughtcrime.securesms.mms.MmsConnection.Apn; +import org.thoughtcrime.securesms.mms.OutgoingMmsConnection; import org.whispersystems.textsecure.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; -import org.thoughtcrime.securesms.mms.MmsDownloadHelper; import org.thoughtcrime.securesms.mms.MmsRadio; import org.thoughtcrime.securesms.mms.MmsRadioException; -import org.thoughtcrime.securesms.mms.MmsSendHelper; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.protocol.WirePrefix; import java.io.IOException; -import java.util.LinkedList; -import java.util.List; import ws.com.google.android.mms.InvalidHeaderValueException; import ws.com.google.android.mms.MmsException; @@ -70,7 +70,7 @@ public class MmsDownloader { } private void handleMmsPendingApnDownloads(MasterSecret masterSecret) { - if (!MmsDownloadHelper.isMmsConnectionParametersAvailable(context, null)) + if (!IncomingMmsConnection.isConnectionPossible(context, null)) return; MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); @@ -135,7 +135,6 @@ public class MmsDownloader { retrieveAndStore(masterSecret, messageId, threadId, contentLocation, transactionId, true, false); radio.disconnect(); - return; } catch (IOException e) { Log.w("MmsDownloader", e); radio.disconnect(); @@ -169,9 +168,10 @@ public class MmsDownloader { boolean radioEnabled, boolean useProxy) throws IOException, MmsException, ApnUnavailableException { - RetrieveConf retrieved = MmsDownloadHelper.retrieveMms(context, contentLocation, - radio.getApnInformation(), - radioEnabled, useProxy); + Apn dbApn = MmsConnection.getApn(context, radio.getApnInformation()); + Apn contentApn = new Apn(contentLocation, dbApn.getProxy(), Integer.toString(dbApn.getPort())); + IncomingMmsConnection connection = new IncomingMmsConnection(context, contentApn); + RetrieveConf retrieved = connection.retrieve(radioEnabled, useProxy); storeRetrievedMms(masterSecret, contentLocation, messageId, threadId, retrieved); sendRetrievedAcknowledgement(transactionId, radioEnabled, useProxy); @@ -206,14 +206,15 @@ public class MmsDownloader { private void sendRetrievedAcknowledgement(byte[] transactionId, boolean usingRadio, boolean useProxy) + throws ApnUnavailableException { try { NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION, transactionId, PduHeaders.STATUS_RETRIEVED); - MmsSendHelper.sendNotificationReceived(context, new PduComposer(context, notifyResponse).make(), - radio.getApnInformation(), usingRadio, useProxy); + OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), new PduComposer(context, notifyResponse).make()); + connection.sendNotificationReceived(usingRadio, useProxy); } catch (InvalidHeaderValueException e) { Log.w("MmsDownloader", e); } catch (IOException e) { diff --git a/src/org/thoughtcrime/securesms/transport/MmsTransport.java b/src/org/thoughtcrime/securesms/transport/MmsTransport.java index 86592812b2..0683bedfec 100644 --- a/src/org/thoughtcrime/securesms/transport/MmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/MmsTransport.java @@ -22,17 +22,17 @@ import android.telephony.TelephonyManager; import android.util.Log; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.MmsRadio; import org.thoughtcrime.securesms.mms.MmsRadioException; -import org.thoughtcrime.securesms.mms.MmsSendHelper; import org.thoughtcrime.securesms.mms.MmsSendResult; +import org.thoughtcrime.securesms.mms.OutgoingMmsConnection; import org.thoughtcrime.securesms.mms.TextTransport; import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.util.NumberUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; @@ -124,21 +124,25 @@ public class MmsTransport { message.setFrom(new EncodedStringValue(number)); } - SendConf conf = MmsSendHelper.sendMms(context, new PduComposer(context, message).make(), - radio.getApnInformation(), usingMmsRadio, useProxy); + try { + OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), new PduComposer(context, message).make()); + SendConf conf = connection.send(usingMmsRadio, useProxy); - for (int i=0;i