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