1 /*
2  * Copyright (C) 2017 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.incallui.calllocation.impl;
18 
19 import android.content.Context;
20 import android.net.Uri;
21 import android.net.Uri.Builder;
22 import android.os.SystemClock;
23 import android.util.Pair;
24 import com.android.dialer.common.LogUtil;
25 import com.android.dialer.util.DialerUtils;
26 import com.android.dialer.util.MoreStrings;
27 import com.google.android.common.http.UrlRules;
28 import java.io.ByteArrayOutputStream;
29 import java.io.FilterInputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.net.HttpURLConnection;
33 import java.net.MalformedURLException;
34 import java.net.ProtocolException;
35 import java.net.URL;
36 import java.util.List;
37 import java.util.Objects;
38 import java.util.Set;
39 
40 /** Utility for making http requests. */
41 public class HttpFetcher {
42 
43   // Phone number
44   public static final String PARAM_ID = "id";
45   // auth token
46   public static final String PARAM_ACCESS_TOKEN = "access_token";
47   private static final String TAG = HttpFetcher.class.getSimpleName();
48 
49   /**
50    * Send a http request to the given url.
51    *
52    * @param urlString The url to request.
53    * @return The response body as a byte array. Or {@literal null} if status code is not 2xx.
54    * @throws java.io.IOException when an error occurs.
55    */
sendRequestAsByteArray( Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)56   public static byte[] sendRequestAsByteArray(
57       Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
58       throws IOException, AuthException {
59     Objects.requireNonNull(urlString);
60 
61     URL url = reWriteUrl(context, urlString);
62     if (url == null) {
63       return null;
64     }
65 
66     HttpURLConnection conn = null;
67     InputStream is = null;
68     boolean isError = false;
69     final long start = SystemClock.uptimeMillis();
70     try {
71       conn = (HttpURLConnection) url.openConnection();
72       setMethodAndHeaders(conn, requestMethod, headers);
73       int responseCode = conn.getResponseCode();
74       LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode);
75       // All 2xx codes are successful.
76       if (responseCode / 100 == 2) {
77         is = conn.getInputStream();
78       } else {
79         is = conn.getErrorStream();
80         isError = true;
81       }
82 
83       final ByteArrayOutputStream baos = new ByteArrayOutputStream();
84       final byte[] buffer = new byte[1024];
85       int bytesRead;
86 
87       while ((bytesRead = is.read(buffer)) != -1) {
88         baos.write(buffer, 0, bytesRead);
89       }
90 
91       if (isError) {
92         handleBadResponse(url.toString(), baos.toByteArray());
93         if (responseCode == 401) {
94           throw new AuthException("Auth error");
95         }
96         return null;
97       }
98 
99       byte[] response = baos.toByteArray();
100       LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes");
101       long end = SystemClock.uptimeMillis();
102       LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms");
103       return response;
104     } finally {
105       DialerUtils.closeQuietly(is);
106       if (conn != null) {
107         conn.disconnect();
108       }
109     }
110   }
111 
112   /**
113    * Send a http request to the given url.
114    *
115    * @return The response body as a InputStream. Or {@literal null} if status code is not 2xx.
116    * @throws java.io.IOException when an error occurs.
117    */
sendRequestAsInputStream( Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)118   public static InputStream sendRequestAsInputStream(
119       Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
120       throws IOException, AuthException {
121     Objects.requireNonNull(urlString);
122 
123     URL url = reWriteUrl(context, urlString);
124     if (url == null) {
125       return null;
126     }
127 
128     HttpURLConnection httpUrlConnection = null;
129     boolean isSuccess = false;
130     try {
131       httpUrlConnection = (HttpURLConnection) url.openConnection();
132       setMethodAndHeaders(httpUrlConnection, requestMethod, headers);
133       int responseCode = httpUrlConnection.getResponseCode();
134       LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode);
135 
136       if (responseCode == 401) {
137         throw new AuthException("Auth error");
138       } else if (responseCode / 100 == 2) { // All 2xx codes are successful.
139         InputStream is = httpUrlConnection.getInputStream();
140         if (is != null) {
141           is = new HttpInputStreamWrapper(httpUrlConnection, is);
142           isSuccess = true;
143           return is;
144         }
145       }
146 
147       return null;
148     } finally {
149       if (httpUrlConnection != null && !isSuccess) {
150         httpUrlConnection.disconnect();
151       }
152     }
153   }
154 
155   /**
156    * Set http method and headers.
157    *
158    * @param conn The connection to add headers to.
159    * @param requestMethod request method
160    * @param headers http headers where the first item in the pair is the key and second item is the
161    *     value.
162    */
setMethodAndHeaders( HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)163   private static void setMethodAndHeaders(
164       HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)
165       throws ProtocolException {
166     conn.setRequestMethod(requestMethod);
167     if (headers != null) {
168       for (Pair<String, String> pair : headers) {
169         conn.setRequestProperty(pair.first, pair.second);
170       }
171     }
172   }
173 
obfuscateUrl(String urlString)174   private static String obfuscateUrl(String urlString) {
175     final Uri uri = Uri.parse(urlString);
176     final Builder builder =
177         new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath());
178     final Set<String> names = uri.getQueryParameterNames();
179     for (String name : names) {
180       if (PARAM_ACCESS_TOKEN.equals(name)) {
181         builder.appendQueryParameter(name, "token");
182       } else {
183         final String value = uri.getQueryParameter(name);
184         if (PARAM_ID.equals(name)) {
185           builder.appendQueryParameter(name, MoreStrings.toSafeString(value));
186         } else {
187           builder.appendQueryParameter(name, value);
188         }
189       }
190     }
191     return builder.toString();
192   }
193 
194   /** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */
getRequestAsString(Context context, String urlString)195   public static String getRequestAsString(Context context, String urlString)
196       throws IOException, AuthException {
197     return getRequestAsString(context, urlString, "GET" /* Default to get. */, null);
198   }
199 
200   /**
201    * Send a http request to the given url.
202    *
203    * @param context The android context.
204    * @param urlString The url to request.
205    * @param headers Http headers to pass in the request. {@literal null} is allowed.
206    * @return The response body as a String. Or {@literal null} if status code is not 2xx.
207    * @throws java.io.IOException when an error occurs.
208    */
getRequestAsString( Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)209   public static String getRequestAsString(
210       Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
211       throws IOException, AuthException {
212     final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers);
213     if (byteArr == null) {
214       // Encountered error response... just return.
215       return null;
216     }
217     final String response = new String(byteArr);
218     LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response);
219     return response;
220   }
221 
222   /**
223    * Lookup up url re-write rules from gServices and apply to the given url.
224    *
225    * @return The new url.
226    */
reWriteUrl(Context context, String url)227   private static URL reWriteUrl(Context context, String url) {
228     final UrlRules rules = UrlRules.getRules(context.getContentResolver());
229     final UrlRules.Rule rule = rules.matchRule(url);
230     final String newUrl = rule.apply(url);
231 
232     if (newUrl == null) {
233       if (LogUtil.isDebugEnabled()) {
234         // Url is blocked by re-write.
235         LogUtil.i(
236             "HttpFetcher.reWriteUrl",
237             "url " + obfuscateUrl(url) + " is blocked.  Ignoring request.");
238       }
239       return null;
240     }
241 
242     if (LogUtil.isDebugEnabled()) {
243       LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
244       if (!newUrl.equals(url)) {
245         LogUtil.i(
246             "HttpFetcher.reWriteUrl",
247             "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
248       }
249     }
250 
251     URL urlObject = null;
252     try {
253       urlObject = new URL(newUrl);
254     } catch (MalformedURLException e) {
255       LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
256     }
257     return urlObject;
258   }
259 
handleBadResponse(String url, byte[] response)260   private static void handleBadResponse(String url, byte[] response) {
261     LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
262     LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
263   }
264 
265   /** Disconnect {@link HttpURLConnection} when InputStream is closed */
266   private static class HttpInputStreamWrapper extends FilterInputStream {
267 
268     final HttpURLConnection httpUrlConnection;
269     final long startMillis = SystemClock.uptimeMillis();
270 
HttpInputStreamWrapper(HttpURLConnection conn, InputStream in)271     public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
272       super(in);
273       httpUrlConnection = conn;
274     }
275 
276     @Override
close()277     public void close() throws IOException {
278       super.close();
279       httpUrlConnection.disconnect();
280       if (LogUtil.isDebugEnabled()) {
281         long endMillis = SystemClock.uptimeMillis();
282         LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - startMillis) + " ms");
283       }
284     }
285   }
286 }
287