/**
* 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.support.annotation.Nullable;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import org.apache.http.Header;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
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.BasicCredentialsProvider;
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.apache.http.message.BasicHeader;
import org.thoughtcrime.securesms.database.ApnDatabase;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.util.guava.Optional;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
@SuppressWarnings("deprecation")
public abstract class LegacyMmsConnection {
public static final String USER_AGENT = "Android-Mms/2.0";
private static final String TAG = LegacyMmsConnection.class.getSimpleName();
protected final Context context;
protected final Apn apn;
protected LegacyMmsConnection(Context context) throws ApnUnavailableException {
this.context = context;
this.apn = getApn(context);
}
public static Apn getApn(Context context) throws ApnUnavailableException {
try {
Optional params = ApnDatabase.getInstance(context)
.getMmsConnectionParameters(TelephonyUtil.getMccMnc(context),
TelephonyUtil.getApn(context));
if (!params.isPresent()) {
throw new ApnUnavailableException("No parameters available from ApnDefaults.");
}
return params.get();
} catch (IOException ioe) {
throw new ApnUnavailableException("ApnDatabase threw an IOException", ioe);
}
}
protected boolean isCdmaDevice() {
return ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
}
@SuppressWarnings("TryWithIdenticalCatches")
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;
}
if (inetAddress == null) {
throw new IOException("Unable to lookup host: InetAddress.getByName() returned null.");
}
byte[] ipAddressBytes = inetAddress.getAddress();
if (ipAddressBytes == null) {
Log.w(TAG, "resolved IP address bytes are null, returning true to attempt a connection anyway.");
return true;
}
Log.w(TAG, "Checking route to address: " + host + ", " + inetAddress.getHostAddress());
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
try {
final Method requestRouteMethod = manager.getClass().getMethod("requestRouteToHostAddress", Integer.TYPE, InetAddress.class);
final boolean routeToHostObtained = (Boolean) requestRouteMethod.invoke(manager, MmsRadio.TYPE_MOBILE_MMS, inetAddress);
Log.w(TAG, "requestRouteToHostAddress(" + inetAddress + ") -> " + routeToHostObtained);
return routeToHostObtained;
} catch (NoSuchMethodException nsme) {
Log.w(TAG, nsme);
} catch (IllegalAccessException iae) {
Log.w(TAG, iae);
} catch (InvocationTargetException ite) {
Log.w(TAG, ite);
}
final int ipAddress = Conversions.byteArrayToIntLittleEndian(ipAddressBytes, 0);
final boolean routeToHostObtained = manager.requestRouteToHost(MmsRadio.TYPE_MOBILE_MMS, ipAddress);
Log.w(TAG, "requestRouteToHost(" + ipAddress + ") -> " + 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();
URL mmsc = new URL(apn.getMmsc());
CredentialsProvider credsProvider = new BasicCredentialsProvider();
if (apn.hasAuthentication()) {
credsProvider.setCredentials(new AuthScope(mmsc.getHost(), mmsc.getPort() > -1 ? mmsc.getPort() : mmsc.getDefaultPort()),
new UsernamePasswordCredentials(apn.getUsername(), apn.getPassword()));
}
return HttpClients.custom()
.setConnectionReuseStrategy(new NoConnectionReuseStrategyHC4())
.setRedirectStrategy(new LaxRedirectStrategy())
.setUserAgent(TextSecurePreferences.getMmsUserAgent(context, USER_AGENT))
.setConnectionManager(new BasicHttpClientConnectionManager())
.setDefaultRequestConfig(config)
.setDefaultCredentialsProvider(credsProvider)
.build();
}
protected byte[] execute(HttpUriRequest request) throws IOException {
Log.w(TAG, "connecting to " + apn.getMmsc());
CloseableHttpClient client = null;
CloseableHttpResponse response = null;
try {
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 List getBaseHeaders() {
final String number = TelephonyUtil.getManager(context).getLine1Number(); ;
return new LinkedList() {{
add(new BasicHeader("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"));
add(new BasicHeader("x-wap-profile", "http://www.google.com/oha/rdf/ua-profile-kila.xml"));
add(new BasicHeader("Content-Type", "application/vnd.wap.mms-message"));
add(new BasicHeader("x-carrier-magic", "http://magic.google.com"));
if (!TextUtils.isEmpty(number)) {
add(new BasicHeader("x-up-calling-line-id", number));
add(new BasicHeader("X-MDN", number));
}
}};
}
public static class Apn {
public static Apn EMPTY = new Apn("", "", "", "", "");
private final String mmsc;
private final String proxy;
private final String port;
private final String username;
private final String password;
public Apn(String mmsc, String proxy, String port, String username, String password) {
this.mmsc = mmsc;
this.proxy = proxy;
this.port = port;
this.username = username;
this.password = password;
}
public Apn(Apn customApn, Apn defaultApn,
boolean useCustomMmsc,
boolean useCustomProxy,
boolean useCustomProxyPort,
boolean useCustomUsername,
boolean useCustomPassword)
{
this.mmsc = useCustomMmsc ? customApn.mmsc : defaultApn.mmsc;
this.proxy = useCustomProxy ? customApn.proxy : defaultApn.proxy;
this.port = useCustomProxyPort ? customApn.port : defaultApn.port;
this.username = useCustomUsername ? customApn.username : defaultApn.username;
this.password = useCustomPassword ? customApn.password : defaultApn.password;
}
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);
}
public boolean hasAuthentication() {
return !TextUtils.isEmpty(username);
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
@Override
public String toString() {
return Apn.class.getSimpleName() +
"{ mmsc: \"" + mmsc + "\"" +
", proxy: " + (proxy == null ? "none" : '"' + proxy + '"') +
", port: " + (port == null ? "(none)" : port) +
", user: " + (username == null ? "none" : '"' + username + '"') +
", pass: " + (password == null ? "none" : '"' + password + '"') + " }";
}
}
}