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