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