1 /* 2 * Copyright (C) 2011 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.volley.toolbox; 18 19 import androidx.annotation.VisibleForTesting; 20 import com.android.volley.AuthFailureError; 21 import com.android.volley.Header; 22 import com.android.volley.Request; 23 import com.android.volley.Request.Method; 24 import java.io.DataOutputStream; 25 import java.io.FilterInputStream; 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.net.HttpURLConnection; 29 import java.net.URL; 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Map; 34 import javax.net.ssl.HttpsURLConnection; 35 import javax.net.ssl.SSLSocketFactory; 36 37 /** A {@link BaseHttpStack} based on {@link HttpURLConnection}. */ 38 public class HurlStack extends BaseHttpStack { 39 40 private static final int HTTP_CONTINUE = 100; 41 42 /** An interface for transforming URLs before use. */ 43 public interface UrlRewriter { 44 /** 45 * Returns a URL to use instead of the provided one, or null to indicate this URL should not 46 * be used at all. 47 */ rewriteUrl(String originalUrl)48 String rewriteUrl(String originalUrl); 49 } 50 51 private final UrlRewriter mUrlRewriter; 52 private final SSLSocketFactory mSslSocketFactory; 53 HurlStack()54 public HurlStack() { 55 this(/* urlRewriter = */ null); 56 } 57 58 /** @param urlRewriter Rewriter to use for request URLs */ HurlStack(UrlRewriter urlRewriter)59 public HurlStack(UrlRewriter urlRewriter) { 60 this(urlRewriter, /* sslSocketFactory = */ null); 61 } 62 63 /** 64 * @param urlRewriter Rewriter to use for request URLs 65 * @param sslSocketFactory SSL factory to use for HTTPS connections 66 */ HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory)67 public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { 68 mUrlRewriter = urlRewriter; 69 mSslSocketFactory = sslSocketFactory; 70 } 71 72 @Override executeRequest(Request<?> request, Map<String, String> additionalHeaders)73 public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders) 74 throws IOException, AuthFailureError { 75 String url = request.getUrl(); 76 HashMap<String, String> map = new HashMap<>(); 77 map.putAll(additionalHeaders); 78 // Request.getHeaders() takes precedence over the given additional (cache) headers). 79 map.putAll(request.getHeaders()); 80 if (mUrlRewriter != null) { 81 String rewritten = mUrlRewriter.rewriteUrl(url); 82 if (rewritten == null) { 83 throw new IOException("URL blocked by rewriter: " + url); 84 } 85 url = rewritten; 86 } 87 URL parsedUrl = new URL(url); 88 HttpURLConnection connection = openConnection(parsedUrl, request); 89 boolean keepConnectionOpen = false; 90 try { 91 for (String headerName : map.keySet()) { 92 connection.setRequestProperty(headerName, map.get(headerName)); 93 } 94 setConnectionParametersForRequest(connection, request); 95 // Initialize HttpResponse with data from the HttpURLConnection. 96 int responseCode = connection.getResponseCode(); 97 if (responseCode == -1) { 98 // -1 is returned by getResponseCode() if the response code could not be retrieved. 99 // Signal to the caller that something was wrong with the connection. 100 throw new IOException("Could not retrieve response code from HttpUrlConnection."); 101 } 102 103 if (!hasResponseBody(request.getMethod(), responseCode)) { 104 return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields())); 105 } 106 107 // Need to keep the connection open until the stream is consumed by the caller. Wrap the 108 // stream such that close() will disconnect the connection. 109 keepConnectionOpen = true; 110 return new HttpResponse( 111 responseCode, 112 convertHeaders(connection.getHeaderFields()), 113 connection.getContentLength(), 114 new UrlConnectionInputStream(connection)); 115 } finally { 116 if (!keepConnectionOpen) { 117 connection.disconnect(); 118 } 119 } 120 } 121 122 @VisibleForTesting convertHeaders(Map<String, List<String>> responseHeaders)123 static List<Header> convertHeaders(Map<String, List<String>> responseHeaders) { 124 List<Header> headerList = new ArrayList<>(responseHeaders.size()); 125 for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) { 126 // HttpUrlConnection includes the status line as a header with a null key; omit it here 127 // since it's not really a header and the rest of Volley assumes non-null keys. 128 if (entry.getKey() != null) { 129 for (String value : entry.getValue()) { 130 headerList.add(new Header(entry.getKey(), value)); 131 } 132 } 133 } 134 return headerList; 135 } 136 137 /** 138 * Checks if a response message contains a body. 139 * 140 * @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3">RFC 7230 section 3.3</a> 141 * @param requestMethod request method 142 * @param responseCode response status code 143 * @return whether the response has a body 144 */ hasResponseBody(int requestMethod, int responseCode)145 private static boolean hasResponseBody(int requestMethod, int responseCode) { 146 return requestMethod != Request.Method.HEAD 147 && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK) 148 && responseCode != HttpURLConnection.HTTP_NO_CONTENT 149 && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED; 150 } 151 152 /** 153 * Wrapper for a {@link HttpURLConnection}'s InputStream which disconnects the connection on 154 * stream close. 155 */ 156 static class UrlConnectionInputStream extends FilterInputStream { 157 private final HttpURLConnection mConnection; 158 UrlConnectionInputStream(HttpURLConnection connection)159 UrlConnectionInputStream(HttpURLConnection connection) { 160 super(inputStreamFromConnection(connection)); 161 mConnection = connection; 162 } 163 164 @Override close()165 public void close() throws IOException { 166 super.close(); 167 mConnection.disconnect(); 168 } 169 } 170 171 /** 172 * Initializes an {@link InputStream} from the given {@link HttpURLConnection}. 173 * 174 * @param connection 175 * @return an HttpEntity populated with data from <code>connection</code>. 176 */ inputStreamFromConnection(HttpURLConnection connection)177 private static InputStream inputStreamFromConnection(HttpURLConnection connection) { 178 InputStream inputStream; 179 try { 180 inputStream = connection.getInputStream(); 181 } catch (IOException ioe) { 182 inputStream = connection.getErrorStream(); 183 } 184 return inputStream; 185 } 186 187 /** Create an {@link HttpURLConnection} for the specified {@code url}. */ createConnection(URL url)188 protected HttpURLConnection createConnection(URL url) throws IOException { 189 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 190 191 // Workaround for the M release HttpURLConnection not observing the 192 // HttpURLConnection.setFollowRedirects() property. 193 // https://code.google.com/p/android/issues/detail?id=194495 194 connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects()); 195 196 return connection; 197 } 198 199 /** 200 * Opens an {@link HttpURLConnection} with parameters. 201 * 202 * @param url 203 * @return an open connection 204 * @throws IOException 205 */ openConnection(URL url, Request<?> request)206 private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException { 207 HttpURLConnection connection = createConnection(url); 208 209 int timeoutMs = request.getTimeoutMs(); 210 connection.setConnectTimeout(timeoutMs); 211 connection.setReadTimeout(timeoutMs); 212 connection.setUseCaches(false); 213 connection.setDoInput(true); 214 215 // use caller-provided custom SslSocketFactory, if any, for HTTPS 216 if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) { 217 ((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory); 218 } 219 220 return connection; 221 } 222 223 // NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be 224 // checked against the existing properties in the connection and not overridden if already set. 225 @SuppressWarnings("deprecation") setConnectionParametersForRequest( HttpURLConnection connection, Request<?> request)226 /* package */ static void setConnectionParametersForRequest( 227 HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError { 228 switch (request.getMethod()) { 229 case Method.DEPRECATED_GET_OR_POST: 230 // This is the deprecated way that needs to be handled for backwards compatibility. 231 // If the request's post body is null, then the assumption is that the request is 232 // GET. Otherwise, it is assumed that the request is a POST. 233 byte[] postBody = request.getPostBody(); 234 if (postBody != null) { 235 connection.setRequestMethod("POST"); 236 addBody(connection, request, postBody); 237 } 238 break; 239 case Method.GET: 240 // Not necessary to set the request method because connection defaults to GET but 241 // being explicit here. 242 connection.setRequestMethod("GET"); 243 break; 244 case Method.DELETE: 245 connection.setRequestMethod("DELETE"); 246 break; 247 case Method.POST: 248 connection.setRequestMethod("POST"); 249 addBodyIfExists(connection, request); 250 break; 251 case Method.PUT: 252 connection.setRequestMethod("PUT"); 253 addBodyIfExists(connection, request); 254 break; 255 case Method.HEAD: 256 connection.setRequestMethod("HEAD"); 257 break; 258 case Method.OPTIONS: 259 connection.setRequestMethod("OPTIONS"); 260 break; 261 case Method.TRACE: 262 connection.setRequestMethod("TRACE"); 263 break; 264 case Method.PATCH: 265 connection.setRequestMethod("PATCH"); 266 addBodyIfExists(connection, request); 267 break; 268 default: 269 throw new IllegalStateException("Unknown method type."); 270 } 271 } 272 addBodyIfExists(HttpURLConnection connection, Request<?> request)273 private static void addBodyIfExists(HttpURLConnection connection, Request<?> request) 274 throws IOException, AuthFailureError { 275 byte[] body = request.getBody(); 276 if (body != null) { 277 addBody(connection, request, body); 278 } 279 } 280 addBody(HttpURLConnection connection, Request<?> request, byte[] body)281 private static void addBody(HttpURLConnection connection, Request<?> request, byte[] body) 282 throws IOException { 283 // Prepare output. There is no need to set Content-Length explicitly, 284 // since this is handled by HttpURLConnection using the size of the prepared 285 // output stream. 286 connection.setDoOutput(true); 287 // Set the content-type unless it was already set (by Request#getHeaders). 288 if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) { 289 connection.setRequestProperty( 290 HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType()); 291 } 292 DataOutputStream out = new DataOutputStream(connection.getOutputStream()); 293 out.write(body); 294 out.close(); 295 } 296 } 297