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 * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules 227 * 228 * @return The new url. 229 */ 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 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 274 public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) { 275 super(in); 276 mHttpUrlConnection = conn; 277 } 278 279 @Override 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