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 com.android.mms.service.exception.MmsHttpException;
     20 import com.android.mms.service.http.NameResolver;
     21 import com.android.mms.service.http.NetworkAwareHttpClient;
     22 
     23 import org.apache.http.Header;
     24 import org.apache.http.HttpEntity;
     25 import org.apache.http.HttpHost;
     26 import org.apache.http.HttpRequest;
     27 import org.apache.http.HttpResponse;
     28 import org.apache.http.StatusLine;
     29 import org.apache.http.client.methods.HttpGet;
     30 import org.apache.http.client.methods.HttpPost;
     31 import org.apache.http.conn.params.ConnRouteParams;
     32 import org.apache.http.entity.ByteArrayEntity;
     33 import org.apache.http.params.HttpConnectionParams;
     34 import org.apache.http.params.HttpParams;
     35 import org.apache.http.params.HttpProtocolParams;
     36 
     37 import android.content.Context;
     38 import android.net.http.AndroidHttpClient;
     39 import android.text.TextUtils;
     40 import android.util.Log;
     41 
     42 import java.io.DataInputStream;
     43 import java.io.IOException;
     44 import java.net.URI;
     45 import java.net.URISyntaxException;
     46 import java.util.Locale;
     47 import java.util.regex.Matcher;
     48 import java.util.regex.Pattern;
     49 
     50 /**
     51  * HTTP utils to make HTTP request to MMSC
     52  */
     53 public class HttpUtils {
     54     private static final String TAG = MmsService.TAG;
     55 
     56     public static final int HTTP_POST_METHOD = 1;
     57     public static final int HTTP_GET_METHOD = 2;
     58 
     59     // Definition for necessary HTTP headers.
     60     private static final String HDR_KEY_ACCEPT = "Accept";
     61     private static final String HDR_KEY_ACCEPT_LANGUAGE = "Accept-Language";
     62 
     63     private static final String HDR_VALUE_ACCEPT =
     64         "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic";
     65 
     66     private HttpUtils() {
     67         // To forbidden instantiate this class.
     68     }
     69 
     70     /**
     71      * A helper method to send or retrieve data through HTTP protocol.
     72      *
     73      * @param url The URL used in a GET request. Null when the method is
     74      *         HTTP_POST_METHOD.
     75      * @param pdu The data to be POST. Null when the method is HTTP_GET_METHOD.
     76      * @param method HTTP_POST_METHOD or HTTP_GET_METHOD.
     77      * @param isProxySet If proxy is set
     78      * @param proxyHost The host of the proxy
     79      * @param proxyPort The port of the proxy
     80      * @param resolver The custom name resolver to use
     81      * @param useIpv6 If we should use IPv6 address when the HTTP client resolves the host name
     82      * @param mmsConfig The MmsConfig to use
     83      * @return A byte array which contains the response data.
     84      *         If an HTTP error code is returned, an IOException will be thrown.
     85      * @throws com.android.mms.service.exception.MmsHttpException if HTTP request gets error response (>=400)
     86      */
     87     public static byte[] httpConnection(Context context, String url, byte[] pdu, int method,
     88             boolean isProxySet, String proxyHost, int proxyPort, NameResolver resolver,
     89             boolean useIpv6, MmsConfig.Overridden mmsConfig) throws MmsHttpException {
     90         final String methodString = getMethodString(method);
     91         if (Log.isLoggable(TAG, Log.VERBOSE)) {
     92             Log.v(TAG, "HttpUtils: request param list\n"
     93                     + "url=" + url + "\n"
     94                     + "method=" + methodString + "\n"
     95                     + "isProxySet=" + isProxySet + "\n"
     96                     + "proxyHost=" + proxyHost + "\n"
     97                     + "proxyPort=" + proxyPort + "\n"
     98                     + "size=" + (pdu != null ? pdu.length : 0));
     99         } else {
    100             Log.d(TAG, "HttpUtils: " + methodString + " " + url);
    101         }
    102 
    103         NetworkAwareHttpClient client = null;
    104         try {
    105             // Make sure to use a proxy which supports CONNECT.
    106             URI hostUrl = new URI(url);
    107             HttpHost target = new HttpHost(hostUrl.getHost(), hostUrl.getPort(),
    108                     HttpHost.DEFAULT_SCHEME_NAME);
    109             client = createHttpClient(context, resolver, useIpv6, mmsConfig);
    110             HttpRequest req = null;
    111 
    112             switch (method) {
    113                 case HTTP_POST_METHOD:
    114                     ByteArrayEntity entity = new ByteArrayEntity(pdu);
    115                     // Set request content type.
    116                     entity.setContentType("application/vnd.wap.mms-message");
    117                     HttpPost post = new HttpPost(url);
    118                     post.setEntity(entity);
    119                     req = post;
    120                     break;
    121                 case HTTP_GET_METHOD:
    122                     req = new HttpGet(url);
    123                     break;
    124             }
    125 
    126             // Set route parameters for the request.
    127             HttpParams params = client.getParams();
    128             if (isProxySet) {
    129                 ConnRouteParams.setDefaultProxy(params, new HttpHost(proxyHost, proxyPort));
    130             }
    131             req.setParams(params);
    132 
    133             // Set necessary HTTP headers for MMS transmission.
    134             req.addHeader(HDR_KEY_ACCEPT, HDR_VALUE_ACCEPT);
    135 
    136             // UA Profile URL header
    137             String xWapProfileTagName = mmsConfig.getUaProfTagName();
    138             String xWapProfileUrl = mmsConfig.getUaProfUrl();
    139             if (xWapProfileUrl != null) {
    140                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    141                     Log.v(TAG, "HttpUtils: xWapProfUrl=" + xWapProfileUrl);
    142                 }
    143                 req.addHeader(xWapProfileTagName, xWapProfileUrl);
    144             }
    145 
    146             // Extra http parameters. Split by '|' to get a list of value pairs.
    147             // Separate each pair by the first occurrence of ':' to obtain a name and
    148             // value. Replace the occurrence of the string returned by
    149             // MmsConfig.getHttpParamsLine1Key() with the users telephone number inside
    150             // the value. And replace the occurrence of the string returned by
    151             // MmsConfig.getHttpParamsNaiKey() with the users NAI(Network Access Identifier)
    152             // inside the value.
    153             String extraHttpParams = mmsConfig.getHttpParams();
    154 
    155             if (!TextUtils.isEmpty(extraHttpParams)) {
    156                 // Parse the parameter list
    157                 String paramList[] = extraHttpParams.split("\\|");
    158                 for (String paramPair : paramList) {
    159                     String splitPair[] = paramPair.split(":", 2);
    160                     if (splitPair.length == 2) {
    161                         final String name = splitPair[0].trim();
    162                         final String value = resolveMacro(context, splitPair[1].trim(), mmsConfig);
    163                         if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) {
    164                             req.addHeader(name, value);
    165                         }
    166                     }
    167                 }
    168             }
    169             req.addHeader(HDR_KEY_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault()));
    170 
    171             final HttpResponse response = client.execute(target, req);
    172             final StatusLine status = response.getStatusLine();
    173             final HttpEntity entity = response.getEntity();
    174             Log.d(TAG, "HttpUtils: status=" + status + " size="
    175                     + (entity != null ? entity.getContentLength() : -1));
    176             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    177                 for (Header header : req.getAllHeaders()) {
    178                     if (header != null) {
    179                         Log.v(TAG, "HttpUtils: header "
    180                                 + header.getName() + "=" + header.getValue());
    181                     }
    182                 }
    183             }
    184             byte[] body = null;
    185             if (entity != null) {
    186                 try {
    187                     if (entity.getContentLength() > 0) {
    188                         body = new byte[(int) entity.getContentLength()];
    189                         DataInputStream dis = new DataInputStream(entity.getContent());
    190                         try {
    191                             dis.readFully(body);
    192                         } finally {
    193                             try {
    194                                 dis.close();
    195                             } catch (IOException e) {
    196                                 Log.e(TAG, "HttpUtils: Error closing input stream: "
    197                                         + e.getMessage());
    198                             }
    199                         }
    200                     }
    201                     if (entity.isChunked()) {
    202                         Log.d(TAG, "HttpUtils: transfer encoding is chunked");
    203                         int bytesTobeRead = mmsConfig.getMaxMessageSize();
    204                         byte[] tempBody = new byte[bytesTobeRead];
    205                         DataInputStream dis = new DataInputStream(entity.getContent());
    206                         try {
    207                             int bytesRead = 0;
    208                             int offset = 0;
    209                             boolean readError = false;
    210                             do {
    211                                 try {
    212                                     bytesRead = dis.read(tempBody, offset, bytesTobeRead);
    213                                 } catch (IOException e) {
    214                                     readError = true;
    215                                     Log.e(TAG, "HttpUtils: error reading input stream", e);
    216                                     break;
    217                                 }
    218                                 if (bytesRead > 0) {
    219                                     bytesTobeRead -= bytesRead;
    220                                     offset += bytesRead;
    221                                 }
    222                             } while (bytesRead >= 0 && bytesTobeRead > 0);
    223                             if (bytesRead == -1 && offset > 0 && !readError) {
    224                                 // offset is same as total number of bytes read
    225                                 // bytesRead will be -1 if the data was read till the eof
    226                                 body = new byte[offset];
    227                                 System.arraycopy(tempBody, 0, body, 0, offset);
    228                                 Log.d(TAG, "HttpUtils: Chunked response length " + offset);
    229                             } else {
    230                                 Log.e(TAG, "HttpUtils: Response entity too large or empty");
    231                             }
    232                         } finally {
    233                             try {
    234                                 dis.close();
    235                             } catch (IOException e) {
    236                                 Log.e(TAG, "HttpUtils: Error closing input stream", e);
    237                             }
    238                         }
    239                     }
    240                 } finally {
    241                     if (entity != null) {
    242                         entity.consumeContent();
    243                     }
    244                 }
    245             }
    246             if (status.getStatusCode() != 200) { // HTTP 200 is success.
    247                 StringBuilder sb = new StringBuilder();
    248                 if (body != null) {
    249                     sb.append("response: text=").append(new String(body)).append('\n');
    250                 }
    251                 for (Header header : req.getAllHeaders()) {
    252                     if (header != null) {
    253                         sb.append("req header: ")
    254                                 .append(header.getName())
    255                                 .append('=')
    256                                 .append(header.getValue())
    257                                 .append('\n');
    258                     }
    259                 }
    260                 for (Header header : response.getAllHeaders()) {
    261                     if (header != null) {
    262                         sb.append("resp header: ")
    263                                 .append(header.getName())
    264                                 .append('=')
    265                                 .append(header.getValue())
    266                                 .append('\n');
    267                     }
    268                 }
    269                 Log.e(TAG, "HttpUtils: error response -- \n"
    270                         + "mStatusCode=" + status.getStatusCode() + "\n"
    271                         + "reason=" + status.getReasonPhrase() + "\n"
    272                         + "url=" + url + "\n"
    273                         + "method=" + methodString + "\n"
    274                         + "isProxySet=" + isProxySet + "\n"
    275                         + "proxyHost=" + proxyHost + "\n"
    276                         + "proxyPort=" + proxyPort
    277                         + (sb != null ? "\n" + sb.toString() : ""));
    278                 throw new MmsHttpException(status.getReasonPhrase());
    279             }
    280             return body;
    281         } catch (IOException e) {
    282             Log.e(TAG, "HttpUtils: IO failure", e);
    283             throw new MmsHttpException(e);
    284         } catch (URISyntaxException e) {
    285             Log.e(TAG, "HttpUtils: invalid url " + url);
    286             throw new MmsHttpException("Invalid url " + url);
    287         } finally {
    288             if (client != null) {
    289                 client.close();
    290             }
    291         }
    292     }
    293 
    294     private static String getMethodString(int method) {
    295         return ((method == HTTP_POST_METHOD) ?
    296                 "POST" : ((method == HTTP_GET_METHOD) ? "GET" : "UNKNOWN"));
    297     }
    298 
    299     /**
    300      * Create an HTTP client
    301      *
    302      * @param context
    303      * @return {@link android.net.http.AndroidHttpClient}
    304      */
    305     private static NetworkAwareHttpClient createHttpClient(Context context, NameResolver resolver,
    306             boolean useIpv6, MmsConfig.Overridden mmsConfig) {
    307         final String userAgent = mmsConfig.getUserAgent();
    308         final NetworkAwareHttpClient client = NetworkAwareHttpClient.newInstance(userAgent, context,
    309                 resolver, useIpv6);
    310         final HttpParams params = client.getParams();
    311         HttpProtocolParams.setContentCharset(params, "UTF-8");
    312 
    313         // set the socket timeout
    314         int soTimeout = mmsConfig.getHttpSocketTimeout();
    315 
    316         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    317             Log.v(TAG, "HttpUtils: createHttpClient w/ socket timeout "
    318                     + soTimeout + " ms, UA=" + userAgent);
    319         }
    320         HttpConnectionParams.setSoTimeout(params, soTimeout);
    321         return client;
    322     }
    323 
    324     private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US";
    325 
    326     /**
    327      * Return the Accept-Language header.  Use the current locale plus
    328      * US if we are in a different locale than US.
    329      * This code copied from the browser's WebSettings.java
    330      *
    331      * @return Current AcceptLanguage String.
    332      */
    333     public static String getCurrentAcceptLanguage(Locale locale) {
    334         final StringBuilder buffer = new StringBuilder();
    335         addLocaleToHttpAcceptLanguage(buffer, locale);
    336 
    337         if (!Locale.US.equals(locale)) {
    338             if (buffer.length() > 0) {
    339                 buffer.append(", ");
    340             }
    341             buffer.append(ACCEPT_LANG_FOR_US_LOCALE);
    342         }
    343 
    344         return buffer.toString();
    345     }
    346 
    347     /**
    348      * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish,
    349      * to new standard.
    350      */
    351     private static String convertObsoleteLanguageCodeToNew(String langCode) {
    352         if (langCode == null) {
    353             return null;
    354         }
    355         if ("iw".equals(langCode)) {
    356             // Hebrew
    357             return "he";
    358         } else if ("in".equals(langCode)) {
    359             // Indonesian
    360             return "id";
    361         } else if ("ji".equals(langCode)) {
    362             // Yiddish
    363             return "yi";
    364         }
    365         return langCode;
    366     }
    367 
    368     private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) {
    369         final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage());
    370         if (language != null) {
    371             builder.append(language);
    372             final String country = locale.getCountry();
    373             if (country != null) {
    374                 builder.append("-");
    375                 builder.append(country);
    376             }
    377         }
    378     }
    379 
    380     private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##");
    381     /**
    382      * Resolve the macro in HTTP param value text
    383      * For example, "something##LINE1##something" is resolved to "something9139531419something"
    384      *
    385      * @param value The HTTP param value possibly containing macros
    386      * @return The HTTP param with macro resolved to real value
    387      */
    388     private static String resolveMacro(Context context, String value,
    389             MmsConfig.Overridden mmsConfig) {
    390         if (TextUtils.isEmpty(value)) {
    391             return value;
    392         }
    393         final Matcher matcher = MACRO_P.matcher(value);
    394         int nextStart = 0;
    395         StringBuilder replaced = null;
    396         while (matcher.find()) {
    397             if (replaced == null) {
    398                 replaced = new StringBuilder();
    399             }
    400             final int matchedStart = matcher.start();
    401             if (matchedStart > nextStart) {
    402                 replaced.append(value.substring(nextStart, matchedStart));
    403             }
    404             final String macro = matcher.group(1);
    405             final String macroValue = mmsConfig.getHttpParamMacro(context, macro);
    406             if (macroValue != null) {
    407                 replaced.append(macroValue);
    408             } else {
    409                 Log.w(TAG, "HttpUtils: invalid macro " + macro);
    410             }
    411             nextStart = matcher.end();
    412         }
    413         if (replaced != null && nextStart < value.length()) {
    414             replaced.append(value.substring(nextStart));
    415         }
    416         return replaced == null ? value : replaced.toString();
    417     }
    418 }
    419