• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.mms.service;
18 
19 import android.content.Context;
20 import android.net.ConnectivityManager;
21 import android.net.LinkProperties;
22 import android.net.Network;
23 import android.os.Bundle;
24 import android.telephony.SmsManager;
25 import android.telephony.SubscriptionManager;
26 import android.telephony.TelephonyManager;
27 import android.text.TextUtils;
28 import android.util.Base64;
29 import android.util.Log;
30 import com.android.mms.service.exception.MmsHttpException;
31 
32 import java.io.BufferedInputStream;
33 import java.io.BufferedOutputStream;
34 import java.io.ByteArrayOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.OutputStream;
38 import java.io.UnsupportedEncodingException;
39 import java.net.HttpURLConnection;
40 import java.net.Inet4Address;
41 import java.net.InetAddress;
42 import java.net.InetSocketAddress;
43 import java.net.MalformedURLException;
44 import java.net.ProtocolException;
45 import java.net.Proxy;
46 import java.net.URL;
47 import java.util.List;
48 import java.util.Locale;
49 import java.util.Map;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52 
53 /**
54  * MMS HTTP client for sending and downloading MMS messages
55  */
56 public class MmsHttpClient {
57     public static final String METHOD_POST = "POST";
58     public static final String METHOD_GET = "GET";
59 
60     private static final String HEADER_CONTENT_TYPE = "Content-Type";
61     private static final String HEADER_ACCEPT = "Accept";
62     private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
63     private static final String HEADER_USER_AGENT = "User-Agent";
64 
65     // The "Accept" header value
66     private static final String HEADER_VALUE_ACCEPT =
67             "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic";
68     // The "Content-Type" header value
69     private static final String HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET =
70             "application/vnd.wap.mms-message; charset=utf-8";
71     private static final String HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET =
72             "application/vnd.wap.mms-message";
73 
74     private static final int IPV4_WAIT_ATTEMPTS = 15;
75     private static final long IPV4_WAIT_DELAY_MS = 1000; // 1 seconds
76 
77     private final Context mContext;
78     private final Network mNetwork;
79     private final ConnectivityManager mConnectivityManager;
80 
81     /**
82      * Constructor
83      *  @param context The Context object
84      * @param network The Network for creating an OKHttp client
85      * @param connectivityManager
86      */
MmsHttpClient(Context context, Network network, ConnectivityManager connectivityManager)87     public MmsHttpClient(Context context, Network network,
88             ConnectivityManager connectivityManager) {
89         mContext = context;
90         mNetwork = network;
91         mConnectivityManager = connectivityManager;
92     }
93 
94     /**
95      * Execute an MMS HTTP request, either a POST (sending) or a GET (downloading)
96      *
97      * @param urlString The request URL, for sending it is usually the MMSC, and for downloading
98      *                  it is the message URL
99      * @param pdu For POST (sending) only, the PDU to send
100      * @param method HTTP method, POST for sending and GET for downloading
101      * @param isProxySet Is there a proxy for the MMSC
102      * @param proxyHost The proxy host
103      * @param proxyPort The proxy port
104      * @param mmsConfig The MMS config to use
105      * @param subId The subscription ID used to get line number, etc.
106      * @param requestId The request ID for logging
107      * @return The HTTP response body
108      * @throws MmsHttpException For any failures
109      */
execute(String urlString, byte[] pdu, String method, boolean isProxySet, String proxyHost, int proxyPort, Bundle mmsConfig, int subId, String requestId)110     public byte[] execute(String urlString, byte[] pdu, String method, boolean isProxySet,
111             String proxyHost, int proxyPort, Bundle mmsConfig, int subId, String requestId)
112             throws MmsHttpException {
113         LogUtil.d(requestId, "HTTP: " + method + " " + redactUrlForNonVerbose(urlString)
114                 + (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "")
115                 + ", PDU size=" + (pdu != null ? pdu.length : 0));
116         checkMethod(method);
117         HttpURLConnection connection = null;
118         try {
119             Proxy proxy = Proxy.NO_PROXY;
120             if (isProxySet) {
121                 proxy = new Proxy(Proxy.Type.HTTP,
122                         new InetSocketAddress(mNetwork.getByName(proxyHost), proxyPort));
123             }
124             final URL url = new URL(urlString);
125             maybeWaitForIpv4(requestId, url);
126             // Now get the connection
127             connection = (HttpURLConnection) mNetwork.openConnection(url, proxy);
128             connection.setDoInput(true);
129             connection.setConnectTimeout(
130                     mmsConfig.getInt(SmsManager.MMS_CONFIG_HTTP_SOCKET_TIMEOUT));
131             // ------- COMMON HEADERS ---------
132             // Header: Accept
133             connection.setRequestProperty(HEADER_ACCEPT, HEADER_VALUE_ACCEPT);
134             // Header: Accept-Language
135             connection.setRequestProperty(
136                     HEADER_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault()));
137             // Header: User-Agent
138             final String userAgent = mmsConfig.getString(SmsManager.MMS_CONFIG_USER_AGENT);
139             LogUtil.i(requestId, "HTTP: User-Agent=" + userAgent);
140             connection.setRequestProperty(HEADER_USER_AGENT, userAgent);
141             // Header: x-wap-profile
142             final String uaProfUrlTagName =
143                     mmsConfig.getString(SmsManager.MMS_CONFIG_UA_PROF_TAG_NAME);
144             final String uaProfUrl = mmsConfig.getString(SmsManager.MMS_CONFIG_UA_PROF_URL);
145             if (uaProfUrl != null) {
146                 LogUtil.i(requestId, "HTTP: UaProfUrl=" + uaProfUrl);
147                 connection.setRequestProperty(uaProfUrlTagName, uaProfUrl);
148             }
149             // Add extra headers specified by mms_config.xml's httpparams
150             addExtraHeaders(connection, mmsConfig, subId);
151             // Different stuff for GET and POST
152             if (METHOD_POST.equals(method)) {
153                 if (pdu == null || pdu.length < 1) {
154                     LogUtil.e(requestId, "HTTP: empty pdu");
155                     throw new MmsHttpException(0/*statusCode*/, "Sending empty PDU");
156                 }
157                 connection.setDoOutput(true);
158                 connection.setRequestMethod(METHOD_POST);
159                 if (mmsConfig.getBoolean(SmsManager.MMS_CONFIG_SUPPORT_HTTP_CHARSET_HEADER)) {
160                     connection.setRequestProperty(HEADER_CONTENT_TYPE,
161                             HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET);
162                 } else {
163                     connection.setRequestProperty(HEADER_CONTENT_TYPE,
164                             HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET);
165                 }
166                 if (LogUtil.isLoggable(Log.VERBOSE)) {
167                     logHttpHeaders(connection.getRequestProperties(), requestId);
168                 }
169                 connection.setFixedLengthStreamingMode(pdu.length);
170                 // Sending request body
171                 final OutputStream out =
172                         new BufferedOutputStream(connection.getOutputStream());
173                 out.write(pdu);
174                 out.flush();
175                 out.close();
176             } else if (METHOD_GET.equals(method)) {
177                 if (LogUtil.isLoggable(Log.VERBOSE)) {
178                     logHttpHeaders(connection.getRequestProperties(), requestId);
179                 }
180                 connection.setRequestMethod(METHOD_GET);
181             }
182             // Get response
183             final int responseCode = connection.getResponseCode();
184             final String responseMessage = connection.getResponseMessage();
185             LogUtil.d(requestId, "HTTP: " + responseCode + " " + responseMessage);
186             if (LogUtil.isLoggable(Log.VERBOSE)) {
187                 logHttpHeaders(connection.getHeaderFields(), requestId);
188             }
189             if (responseCode / 100 != 2) {
190                 throw new MmsHttpException(responseCode, responseMessage);
191             }
192             final InputStream in = new BufferedInputStream(connection.getInputStream());
193             final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
194             final byte[] buf = new byte[4096];
195             int count = 0;
196             while ((count = in.read(buf)) > 0) {
197                 byteOut.write(buf, 0, count);
198             }
199             in.close();
200             final byte[] responseBody = byteOut.toByteArray();
201             LogUtil.d(requestId, "HTTP: response size="
202                     + (responseBody != null ? responseBody.length : 0));
203             return responseBody;
204         } catch (MalformedURLException e) {
205             final String redactedUrl = redactUrlForNonVerbose(urlString);
206             LogUtil.e(requestId, "HTTP: invalid URL " + redactedUrl, e);
207             throw new MmsHttpException(0/*statusCode*/, "Invalid URL " + redactedUrl, e);
208         } catch (ProtocolException e) {
209             final String redactedUrl = redactUrlForNonVerbose(urlString);
210             LogUtil.e(requestId, "HTTP: invalid URL protocol " + redactedUrl, e);
211             throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + redactedUrl, e);
212         } catch (IOException e) {
213             LogUtil.e(requestId, "HTTP: IO failure", e);
214             throw new MmsHttpException(0/*statusCode*/, e);
215         } finally {
216             if (connection != null) {
217                 connection.disconnect();
218             }
219         }
220     }
221 
maybeWaitForIpv4(final String requestId, final URL url)222     private void maybeWaitForIpv4(final String requestId, final URL url) {
223         // If it's a literal IPv4 address and we're on an IPv6-only network,
224         // wait until IPv4 is available.
225         Inet4Address ipv4Literal = null;
226         try {
227             ipv4Literal = (Inet4Address) InetAddress.parseNumericAddress(url.getHost());
228         } catch (IllegalArgumentException | ClassCastException e) {
229             // Ignore
230         }
231         if (ipv4Literal == null) {
232             // Not an IPv4 address.
233             return;
234         }
235         for (int i = 0; i < IPV4_WAIT_ATTEMPTS; i++) {
236             final LinkProperties lp = mConnectivityManager.getLinkProperties(mNetwork);
237             if (lp != null) {
238                 if (!lp.isReachable(ipv4Literal)) {
239                     LogUtil.w(requestId, "HTTP: IPv4 not yet provisioned");
240                     try {
241                         Thread.sleep(IPV4_WAIT_DELAY_MS);
242                     } catch (InterruptedException e) {
243                         // Ignore
244                     }
245                 } else {
246                     LogUtil.i(requestId, "HTTP: IPv4 provisioned");
247                     break;
248                 }
249             } else {
250                 LogUtil.w(requestId, "HTTP: network disconnected, skip ipv4 check");
251                 break;
252             }
253         }
254     }
255 
logHttpHeaders(Map<String, List<String>> headers, String requestId)256     private static void logHttpHeaders(Map<String, List<String>> headers, String requestId) {
257         final StringBuilder sb = new StringBuilder();
258         if (headers != null) {
259             for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
260                 final String key = entry.getKey();
261                 final List<String> values = entry.getValue();
262                 if (values != null) {
263                     for (String value : values) {
264                         sb.append(key).append('=').append(value).append('\n');
265                     }
266                 }
267             }
268             LogUtil.v(requestId, "HTTP: headers\n" + sb.toString());
269         }
270     }
271 
checkMethod(String method)272     private static void checkMethod(String method) throws MmsHttpException {
273         if (!METHOD_GET.equals(method) && !METHOD_POST.equals(method)) {
274             throw new MmsHttpException(0/*statusCode*/, "Invalid method " + method);
275         }
276     }
277 
278     private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US";
279 
280     /**
281      * Return the Accept-Language header.  Use the current locale plus
282      * US if we are in a different locale than US.
283      * This code copied from the browser's WebSettings.java
284      *
285      * @return Current AcceptLanguage String.
286      */
getCurrentAcceptLanguage(Locale locale)287     public static String getCurrentAcceptLanguage(Locale locale) {
288         final StringBuilder buffer = new StringBuilder();
289         addLocaleToHttpAcceptLanguage(buffer, locale);
290 
291         if (!Locale.US.equals(locale)) {
292             if (buffer.length() > 0) {
293                 buffer.append(", ");
294             }
295             buffer.append(ACCEPT_LANG_FOR_US_LOCALE);
296         }
297 
298         return buffer.toString();
299     }
300 
301     /**
302      * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish,
303      * to new standard.
304      */
convertObsoleteLanguageCodeToNew(String langCode)305     private static String convertObsoleteLanguageCodeToNew(String langCode) {
306         if (langCode == null) {
307             return null;
308         }
309         if ("iw".equals(langCode)) {
310             // Hebrew
311             return "he";
312         } else if ("in".equals(langCode)) {
313             // Indonesian
314             return "id";
315         } else if ("ji".equals(langCode)) {
316             // Yiddish
317             return "yi";
318         }
319         return langCode;
320     }
321 
addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale)322     private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) {
323         final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage());
324         if (language != null) {
325             builder.append(language);
326             final String country = locale.getCountry();
327             if (country != null) {
328                 builder.append("-");
329                 builder.append(country);
330             }
331         }
332     }
333 
334     /**
335      * Add extra HTTP headers from mms_config.xml's httpParams, which is a list of key/value
336      * pairs separated by "|". Each key/value pair is separated by ":". Value may contain
337      * macros like "##LINE1##" or "##NAI##" which is resolved with methods in this class
338      *
339      * @param connection The HttpURLConnection that we add headers to
340      * @param mmsConfig The MmsConfig object
341      * @param subId The subscription ID used to get line number, etc.
342      */
addExtraHeaders(HttpURLConnection connection, Bundle mmsConfig, int subId)343     private void addExtraHeaders(HttpURLConnection connection, Bundle mmsConfig, int subId) {
344         final String extraHttpParams = mmsConfig.getString(SmsManager.MMS_CONFIG_HTTP_PARAMS);
345         if (!TextUtils.isEmpty(extraHttpParams)) {
346             // Parse the parameter list
347             String paramList[] = extraHttpParams.split("\\|");
348             for (String paramPair : paramList) {
349                 String splitPair[] = paramPair.split(":", 2);
350                 if (splitPair.length == 2) {
351                     final String name = splitPair[0].trim();
352                     final String value =
353                             resolveMacro(mContext, splitPair[1].trim(), mmsConfig, subId);
354                     if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) {
355                         // Add the header if the param is valid
356                         connection.setRequestProperty(name, value);
357                     }
358                 }
359             }
360         }
361     }
362 
363     private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##");
364     /**
365      * Resolve the macro in HTTP param value text
366      * For example, "something##LINE1##something" is resolved to "something9139531419something"
367      *
368      * @param value The HTTP param value possibly containing macros
369      * @param subId The subscription ID used to get line number, etc.
370      * @return The HTTP param with macros resolved to real value
371      */
resolveMacro(Context context, String value, Bundle mmsConfig, int subId)372     private static String resolveMacro(Context context, String value, Bundle mmsConfig, int subId) {
373         if (TextUtils.isEmpty(value)) {
374             return value;
375         }
376         final Matcher matcher = MACRO_P.matcher(value);
377         int nextStart = 0;
378         StringBuilder replaced = null;
379         while (matcher.find()) {
380             if (replaced == null) {
381                 replaced = new StringBuilder();
382             }
383             final int matchedStart = matcher.start();
384             if (matchedStart > nextStart) {
385                 replaced.append(value.substring(nextStart, matchedStart));
386             }
387             final String macro = matcher.group(1);
388             final String macroValue = getMacroValue(context, macro, mmsConfig, subId);
389             if (macroValue != null) {
390                 replaced.append(macroValue);
391             }
392             nextStart = matcher.end();
393         }
394         if (replaced != null && nextStart < value.length()) {
395             replaced.append(value.substring(nextStart));
396         }
397         return replaced == null ? value : replaced.toString();
398     }
399 
400     /**
401      * Redact the URL for non-VERBOSE logging. Replace url with only the host part and the length
402      * of the input URL string.
403      *
404      * @param urlString
405      * @return
406      */
redactUrlForNonVerbose(String urlString)407     public static String redactUrlForNonVerbose(String urlString) {
408         if (LogUtil.isLoggable(Log.VERBOSE)) {
409             // Don't redact for VERBOSE level logging
410             return urlString;
411         }
412         if (TextUtils.isEmpty(urlString)) {
413             return urlString;
414         }
415         String protocol = "http";
416         String host = "";
417         try {
418             final URL url = new URL(urlString);
419             protocol = url.getProtocol();
420             host = url.getHost();
421         } catch (MalformedURLException e) {
422             // Ignore
423         }
424         // Print "http://host[length]"
425         final StringBuilder sb = new StringBuilder();
426         sb.append(protocol).append("://").append(host)
427                 .append("[").append(urlString.length()).append("]");
428         return sb.toString();
429     }
430 
431     /*
432      * Macro names
433      */
434     // The raw phone number from TelephonyManager.getLine1Number
435     private static final String MACRO_LINE1 = "LINE1";
436     // The phone number without country code
437     private static final String MACRO_LINE1NOCOUNTRYCODE = "LINE1NOCOUNTRYCODE";
438     // NAI (Network Access Identifier), used by Sprint for authentication
439     private static final String MACRO_NAI = "NAI";
440     /**
441      * Return the HTTP param macro value.
442      * Example: "LINE1" returns the phone number, etc.
443      *
444      * @param macro The macro name
445      * @param mmsConfig The MMS config which contains NAI suffix.
446      * @param subId The subscription ID used to get line number, etc.
447      * @return The value of the defined macro
448      */
getMacroValue(Context context, String macro, Bundle mmsConfig, int subId)449     private static String getMacroValue(Context context, String macro, Bundle mmsConfig,
450             int subId) {
451         if (MACRO_LINE1.equals(macro)) {
452             return getLine1(context, subId);
453         } else if (MACRO_LINE1NOCOUNTRYCODE.equals(macro)) {
454             return getLine1NoCountryCode(context, subId);
455         } else if (MACRO_NAI.equals(macro)) {
456             return getNai(context, mmsConfig, subId);
457         }
458         LogUtil.e("Invalid macro " + macro);
459         return null;
460     }
461 
462     /**
463      * Returns the phone number for the given subscription ID.
464      */
getLine1(Context context, int subId)465     private static String getLine1(Context context, int subId) {
466         final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(
467                 Context.TELEPHONY_SERVICE);
468         return telephonyManager.getLine1Number(subId);
469     }
470 
471     /**
472      * Returns the phone number (without country code) for the given subscription ID.
473      */
getLine1NoCountryCode(Context context, int subId)474     private static String getLine1NoCountryCode(Context context, int subId) {
475         final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(
476                 Context.TELEPHONY_SERVICE);
477         return PhoneUtils.getNationalNumber(
478                 telephonyManager,
479                 subId,
480                 telephonyManager.getLine1Number(subId));
481     }
482 
483     /**
484      * Returns the NAI (Network Access Identifier) from SystemProperties for the given subscription
485      * ID.
486      */
getNai(Context context, Bundle mmsConfig, int subId)487     private static String getNai(Context context, Bundle mmsConfig, int subId) {
488         final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(
489                 Context.TELEPHONY_SERVICE);
490         String nai = telephonyManager.getNai(SubscriptionManager.getSlotId(subId));
491         if (LogUtil.isLoggable(Log.VERBOSE)) {
492             LogUtil.v("getNai: nai=" + nai);
493         }
494 
495         if (!TextUtils.isEmpty(nai)) {
496             String naiSuffix = mmsConfig.getString(SmsManager.MMS_CONFIG_NAI_SUFFIX);
497             if (!TextUtils.isEmpty(naiSuffix)) {
498                 nai = nai + naiSuffix;
499             }
500             byte[] encoded = null;
501             try {
502                 encoded = Base64.encode(nai.getBytes("UTF-8"), Base64.NO_WRAP);
503             } catch (UnsupportedEncodingException e) {
504                 encoded = Base64.encode(nai.getBytes(), Base64.NO_WRAP);
505             }
506             try {
507                 nai = new String(encoded, "UTF-8");
508             } catch (UnsupportedEncodingException e) {
509                 nai = new String(encoded);
510             }
511         }
512         return nai;
513     }
514 }
515