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