HttpFetcher.java revision 30ccc4f3aa6da94f0bb8a01a880a6353b883b263
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
17package com.android.incallui.calllocation.impl;
18
19import static com.android.dialer.util.DialerUtils.closeQuietly;
20
21import android.content.Context;
22import android.net.Uri;
23import android.net.Uri.Builder;
24import android.os.SystemClock;
25import android.util.Pair;
26import com.android.dialer.common.LogUtil;
27import com.android.dialer.util.MoreStrings;
28import com.google.android.common.http.UrlRules;
29import java.io.ByteArrayOutputStream;
30import java.io.FilterInputStream;
31import java.io.IOException;
32import java.io.InputStream;
33import java.net.HttpURLConnection;
34import java.net.MalformedURLException;
35import java.net.ProtocolException;
36import java.net.URL;
37import java.util.List;
38import java.util.Objects;
39import java.util.Set;
40
41/** Utility for making http requests. */
42public 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   */
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   */
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   */
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
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. */
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   */
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   * @return The new url.
227   */
228  private static URL reWriteUrl(Context context, String url) {
229    final UrlRules rules = UrlRules.getRules(context.getContentResolver());
230    final UrlRules.Rule rule = rules.matchRule(url);
231    final String newUrl = rule.apply(url);
232
233    if (newUrl == null) {
234      if (LogUtil.isDebugEnabled()) {
235        // Url is blocked by re-write.
236        LogUtil.i(
237            "HttpFetcher.reWriteUrl",
238            "url " + obfuscateUrl(url) + " is blocked.  Ignoring request.");
239      }
240      return null;
241    }
242
243    if (LogUtil.isDebugEnabled()) {
244      LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
245      if (!newUrl.equals(url)) {
246        LogUtil.i(
247            "HttpFetcher.reWriteUrl",
248            "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
249      }
250    }
251
252    URL urlObject = null;
253    try {
254      urlObject = new URL(newUrl);
255    } catch (MalformedURLException e) {
256      LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
257    }
258    return urlObject;
259  }
260
261  private static void handleBadResponse(String url, byte[] response) {
262    LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
263    LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
264  }
265
266  /** Disconnect {@link HttpURLConnection} when InputStream is closed */
267  private static class HttpInputStreamWrapper extends FilterInputStream {
268
269    final HttpURLConnection mHttpUrlConnection;
270    final long mStartMillis = SystemClock.uptimeMillis();
271
272    public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
273      super(in);
274      mHttpUrlConnection = conn;
275    }
276
277    @Override
278    public void close() throws IOException {
279      super.close();
280      mHttpUrlConnection.disconnect();
281      if (LogUtil.isDebugEnabled()) {
282        long endMillis = SystemClock.uptimeMillis();
283        LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - mStartMillis) + " ms");
284      }
285    }
286  }
287}
288