Home | History | Annotate | Download | only in impl
      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    */
     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    */
    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    */
    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 
    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. */
    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    */
    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    */
    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 
    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 
    271     public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
    272       super(in);
    273       httpUrlConnection = conn;
    274     }
    275 
    276     @Override
    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