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