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