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.text.TextUtils; 21 import android.util.Log; 22 23 import com.android.mms.service.exception.MmsHttpException; 24 import com.android.okhttp.ConnectionPool; 25 import com.android.okhttp.HostResolver; 26 import com.android.okhttp.HttpHandler; 27 import com.android.okhttp.HttpsHandler; 28 import com.android.okhttp.OkHttpClient; 29 30 import java.io.BufferedInputStream; 31 import java.io.BufferedOutputStream; 32 import java.io.ByteArrayOutputStream; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.net.HttpURLConnection; 37 import java.net.InetSocketAddress; 38 import java.net.MalformedURLException; 39 import java.net.ProtocolException; 40 import java.net.Proxy; 41 import java.net.URL; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 import java.util.regex.Matcher; 46 import java.util.regex.Pattern; 47 import javax.net.SocketFactory; 48 49 /** 50 * MMS HTTP client for sending and downloading MMS messages 51 */ 52 public class MmsHttpClient { 53 public static final String METHOD_POST = "POST"; 54 public static final String METHOD_GET = "GET"; 55 56 private static final String HEADER_CONTENT_TYPE = "Content-Type"; 57 private static final String HEADER_ACCEPT = "Accept"; 58 private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language"; 59 private static final String HEADER_USER_AGENT = "User-Agent"; 60 61 // The "Accept" header value 62 private static final String HEADER_VALUE_ACCEPT = 63 "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"; 64 // The "Content-Type" header value 65 private static final String HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET = 66 "application/vnd.wap.mms-message; charset=utf-8"; 67 private static final String HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET = 68 "application/vnd.wap.mms-message"; 69 70 private final Context mContext; 71 private final SocketFactory mSocketFactory; 72 private final HostResolver mHostResolver; 73 private final ConnectionPool mConnectionPool; 74 75 /** 76 * Constructor 77 * 78 * @param context The Context object 79 * @param socketFactory The socket factory for creating an OKHttp client 80 * @param hostResolver The host name resolver for creating an OKHttp client 81 * @param connectionPool The connection pool for creating an OKHttp client 82 */ MmsHttpClient(Context context, SocketFactory socketFactory, HostResolver hostResolver, ConnectionPool connectionPool)83 public MmsHttpClient(Context context, SocketFactory socketFactory, HostResolver hostResolver, 84 ConnectionPool connectionPool) { 85 mContext = context; 86 mSocketFactory = socketFactory; 87 mHostResolver = hostResolver; 88 mConnectionPool = connectionPool; 89 } 90 91 /** 92 * Execute an MMS HTTP request, either a POST (sending) or a GET (downloading) 93 * 94 * @param urlString The request URL, for sending it is usually the MMSC, and for downloading 95 * it is the message URL 96 * @param pdu For POST (sending) only, the PDU to send 97 * @param method HTTP method, POST for sending and GET for downloading 98 * @param isProxySet Is there a proxy for the MMSC 99 * @param proxyHost The proxy host 100 * @param proxyPort The proxy port 101 * @param mmsConfig The MMS config to use 102 * @return The HTTP response body 103 * @throws MmsHttpException For any failures 104 */ execute(String urlString, byte[] pdu, String method, boolean isProxySet, String proxyHost, int proxyPort, MmsConfig.Overridden mmsConfig)105 public byte[] execute(String urlString, byte[] pdu, String method, boolean isProxySet, 106 String proxyHost, int proxyPort, MmsConfig.Overridden mmsConfig) 107 throws MmsHttpException { 108 Log.d(MmsService.TAG, "HTTP: " + method + " " + urlString 109 + (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "") 110 + ", PDU size=" + (pdu != null ? pdu.length : 0)); 111 checkMethod(method); 112 HttpURLConnection connection = null; 113 try { 114 Proxy proxy = null; 115 if (isProxySet) { 116 proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); 117 } 118 final URL url = new URL(urlString); 119 // Now get the connection 120 connection = openConnection(url, proxy); 121 connection.setDoInput(true); 122 connection.setConnectTimeout(mmsConfig.getHttpSocketTimeout()); 123 // ------- COMMON HEADERS --------- 124 // Header: Accept 125 connection.setRequestProperty(HEADER_ACCEPT, HEADER_VALUE_ACCEPT); 126 // Header: Accept-Language 127 connection.setRequestProperty( 128 HEADER_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault())); 129 // Header: User-Agent 130 final String userAgent = mmsConfig.getUserAgent(); 131 Log.i(MmsService.TAG, "HTTP: User-Agent=" + userAgent); 132 connection.setRequestProperty(HEADER_USER_AGENT, userAgent); 133 // Header: x-wap-profile 134 final String uaProfUrlTagName = mmsConfig.getUaProfTagName(); 135 final String uaProfUrl = mmsConfig.getUaProfUrl(); 136 if (uaProfUrl != null) { 137 Log.i(MmsService.TAG, "HTTP: UaProfUrl=" + uaProfUrl); 138 connection.setRequestProperty(uaProfUrlTagName, uaProfUrl); 139 } 140 // Add extra headers specified by mms_config.xml's httpparams 141 addExtraHeaders(connection, mmsConfig); 142 // Different stuff for GET and POST 143 if (METHOD_POST.equals(method)) { 144 if (pdu == null || pdu.length < 1) { 145 Log.e(MmsService.TAG, "HTTP: empty pdu"); 146 throw new MmsHttpException(0/*statusCode*/, "Sending empty PDU"); 147 } 148 connection.setDoOutput(true); 149 connection.setRequestMethod(METHOD_POST); 150 if (mmsConfig.getSupportHttpCharsetHeader()) { 151 connection.setRequestProperty(HEADER_CONTENT_TYPE, 152 HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET); 153 } else { 154 connection.setRequestProperty(HEADER_CONTENT_TYPE, 155 HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET); 156 } 157 if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { 158 logHttpHeaders(connection.getRequestProperties()); 159 } 160 connection.setFixedLengthStreamingMode(pdu.length); 161 // Sending request body 162 final OutputStream out = 163 new BufferedOutputStream(connection.getOutputStream()); 164 out.write(pdu); 165 out.flush(); 166 out.close(); 167 } else if (METHOD_GET.equals(method)) { 168 if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { 169 logHttpHeaders(connection.getRequestProperties()); 170 } 171 connection.setRequestMethod(METHOD_GET); 172 } 173 // Get response 174 final int responseCode = connection.getResponseCode(); 175 final String responseMessage = connection.getResponseMessage(); 176 Log.d(MmsService.TAG, "HTTP: " + responseCode + " " + responseMessage); 177 if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { 178 logHttpHeaders(connection.getHeaderFields()); 179 } 180 if (responseCode / 100 != 2) { 181 throw new MmsHttpException(responseCode, responseMessage); 182 } 183 final InputStream in = new BufferedInputStream(connection.getInputStream()); 184 final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 185 final byte[] buf = new byte[4096]; 186 int count = 0; 187 while ((count = in.read(buf)) > 0) { 188 byteOut.write(buf, 0, count); 189 } 190 in.close(); 191 final byte[] responseBody = byteOut.toByteArray(); 192 Log.d(MmsService.TAG, "HTTP: response size=" 193 + (responseBody != null ? responseBody.length : 0)); 194 return responseBody; 195 } catch (MalformedURLException e) { 196 Log.e(MmsService.TAG, "HTTP: invalid URL " + urlString, e); 197 throw new MmsHttpException(0/*statusCode*/, "Invalid URL " + urlString, e); 198 } catch (ProtocolException e) { 199 Log.e(MmsService.TAG, "HTTP: invalid URL protocol " + urlString, e); 200 throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + urlString, e); 201 } catch (IOException e) { 202 Log.e(MmsService.TAG, "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 /** 212 * Open an HTTP connection 213 * 214 * TODO: The following code is borrowed from android.net.Network.openConnection 215 * Once that method supports proxy, we should use that instead 216 * Also we should remove the associated HostResolver and ConnectionPool from 217 * MmsNetworkManager 218 * 219 * @param url The URL to connect to 220 * @param proxy The proxy to use 221 * @return The opened HttpURLConnection 222 * @throws MalformedURLException If URL is malformed 223 */ openConnection(URL url, Proxy proxy)224 private HttpURLConnection openConnection(URL url, Proxy proxy) throws MalformedURLException { 225 final String protocol = url.getProtocol(); 226 OkHttpClient okHttpClient; 227 if (protocol.equals("http")) { 228 okHttpClient = HttpHandler.createHttpOkHttpClient(proxy); 229 } else if (protocol.equals("https")) { 230 okHttpClient = HttpsHandler.createHttpsOkHttpClient(proxy); 231 } else { 232 throw new MalformedURLException("Invalid URL or unrecognized protocol " + protocol); 233 } 234 return okHttpClient.setSocketFactory(mSocketFactory) 235 .setHostResolver(mHostResolver) 236 .setConnectionPool(mConnectionPool) 237 .open(url); 238 } 239 logHttpHeaders(Map<String, List<String>> headers)240 private static void logHttpHeaders(Map<String, List<String>> headers) { 241 final StringBuilder sb = new StringBuilder(); 242 if (headers != null) { 243 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 244 final String key = entry.getKey(); 245 final List<String> values = entry.getValue(); 246 if (values != null) { 247 for (String value : values) { 248 sb.append(key).append('=').append(value).append('\n'); 249 } 250 } 251 } 252 Log.v(MmsService.TAG, "HTTP: headers\n" + sb.toString()); 253 } 254 } 255 checkMethod(String method)256 private static void checkMethod(String method) throws MmsHttpException { 257 if (!METHOD_GET.equals(method) && !METHOD_POST.equals(method)) { 258 throw new MmsHttpException(0/*statusCode*/, "Invalid method " + method); 259 } 260 } 261 262 private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US"; 263 264 /** 265 * Return the Accept-Language header. Use the current locale plus 266 * US if we are in a different locale than US. 267 * This code copied from the browser's WebSettings.java 268 * 269 * @return Current AcceptLanguage String. 270 */ getCurrentAcceptLanguage(Locale locale)271 public static String getCurrentAcceptLanguage(Locale locale) { 272 final StringBuilder buffer = new StringBuilder(); 273 addLocaleToHttpAcceptLanguage(buffer, locale); 274 275 if (!Locale.US.equals(locale)) { 276 if (buffer.length() > 0) { 277 buffer.append(", "); 278 } 279 buffer.append(ACCEPT_LANG_FOR_US_LOCALE); 280 } 281 282 return buffer.toString(); 283 } 284 285 /** 286 * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish, 287 * to new standard. 288 */ convertObsoleteLanguageCodeToNew(String langCode)289 private static String convertObsoleteLanguageCodeToNew(String langCode) { 290 if (langCode == null) { 291 return null; 292 } 293 if ("iw".equals(langCode)) { 294 // Hebrew 295 return "he"; 296 } else if ("in".equals(langCode)) { 297 // Indonesian 298 return "id"; 299 } else if ("ji".equals(langCode)) { 300 // Yiddish 301 return "yi"; 302 } 303 return langCode; 304 } 305 addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale)306 private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) { 307 final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage()); 308 if (language != null) { 309 builder.append(language); 310 final String country = locale.getCountry(); 311 if (country != null) { 312 builder.append("-"); 313 builder.append(country); 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 * @return The HTTP param with macro resolved to real value 325 */ resolveMacro(Context context, String value, MmsConfig.Overridden mmsConfig)326 private static String resolveMacro(Context context, String value, 327 MmsConfig.Overridden mmsConfig) { 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 = mmsConfig.getHttpParamMacro(context, macro); 344 if (macroValue != null) { 345 replaced.append(macroValue); 346 } else { 347 Log.w(MmsService.TAG, "HTTP: invalid macro " + macro); 348 } 349 nextStart = matcher.end(); 350 } 351 if (replaced != null && nextStart < value.length()) { 352 replaced.append(value.substring(nextStart)); 353 } 354 return replaced == null ? value : replaced.toString(); 355 } 356 357 /** 358 * Add extra HTTP headers from mms_config.xml's httpParams, which is a list of key/value 359 * pairs separated by "|". Each key/value pair is separated by ":". Value may contain 360 * macros like "##LINE1##" or "##NAI##" which is resolved with methods in this class 361 * 362 * @param connection The HttpURLConnection that we add headers to 363 * @param mmsConfig The MmsConfig object 364 */ addExtraHeaders(HttpURLConnection connection, MmsConfig.Overridden mmsConfig)365 private void addExtraHeaders(HttpURLConnection connection, MmsConfig.Overridden mmsConfig) { 366 final String extraHttpParams = mmsConfig.getHttpParams(); 367 if (!TextUtils.isEmpty(extraHttpParams)) { 368 // Parse the parameter list 369 String paramList[] = extraHttpParams.split("\\|"); 370 for (String paramPair : paramList) { 371 String splitPair[] = paramPair.split(":", 2); 372 if (splitPair.length == 2) { 373 final String name = splitPair[0].trim(); 374 final String value = resolveMacro(mContext, splitPair[1].trim(), mmsConfig); 375 if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) { 376 // Add the header if the param is valid 377 connection.setRequestProperty(name, value); 378 } 379 } 380 } 381 } 382 } 383 } 384