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