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