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