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.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      */
     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      */
    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 
    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 
    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 
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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.getSlotIndex(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