blob: 35c6a72fcb1b3398e8b2ef282706c8667e0a826d [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley.toolbox;
import androidx.annotation.VisibleForTesting;
import com.android.volley.AuthFailureError;
import com.android.volley.Header;
import com.android.volley.Request;
import com.android.volley.Request.Method;
import java.io.DataOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
/** A {@link BaseHttpStack} based on {@link HttpURLConnection}. */
public class HurlStack extends BaseHttpStack {
private static final int HTTP_CONTINUE = 100;
/** An interface for transforming URLs before use. */
public interface UrlRewriter extends com.android.volley.toolbox.UrlRewriter {}
private final UrlRewriter mUrlRewriter;
private final SSLSocketFactory mSslSocketFactory;
public HurlStack() {
this(/* urlRewriter = */ null);
}
/** @param urlRewriter Rewriter to use for request URLs */
public HurlStack(UrlRewriter urlRewriter) {
this(urlRewriter, /* sslSocketFactory = */ null);
}
/**
* @param urlRewriter Rewriter to use for request URLs
* @param sslSocketFactory SSL factory to use for HTTPS connections
*/
public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
mUrlRewriter = urlRewriter;
mSslSocketFactory = sslSocketFactory;
}
@Override
public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
HashMap<String, String> map = new HashMap<>();
map.putAll(additionalHeaders);
// Request.getHeaders() takes precedence over the given additional (cache) headers).
map.putAll(request.getHeaders());
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
throw new IOException("URL blocked by rewriter: " + url);
}
url = rewritten;
}
URL parsedUrl = new URL(url);
HttpURLConnection connection = openConnection(parsedUrl, request);
boolean keepConnectionOpen = false;
try {
for (String headerName : map.keySet()) {
connection.setRequestProperty(headerName, map.get(headerName));
}
setConnectionParametersForRequest(connection, request);
// Initialize HttpResponse with data from the HttpURLConnection.
int responseCode = connection.getResponseCode();
if (responseCode == -1) {
// -1 is returned by getResponseCode() if the response code could not be retrieved.
// Signal to the caller that something was wrong with the connection.
throw new IOException("Could not retrieve response code from HttpUrlConnection.");
}
if (!hasResponseBody(request.getMethod(), responseCode)) {
return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()));
}
// Need to keep the connection open until the stream is consumed by the caller. Wrap the
// stream such that close() will disconnect the connection.
keepConnectionOpen = true;
return new HttpResponse(
responseCode,
convertHeaders(connection.getHeaderFields()),
connection.getContentLength(),
createInputStream(request, connection));
} finally {
if (!keepConnectionOpen) {
connection.disconnect();
}
}
}
@VisibleForTesting
static List<Header> convertHeaders(Map<String, List<String>> responseHeaders) {
List<Header> headerList = new ArrayList<>(responseHeaders.size());
for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) {
// HttpUrlConnection includes the status line as a header with a null key; omit it here
// since it's not really a header and the rest of Volley assumes non-null keys.
if (entry.getKey() != null) {
for (String value : entry.getValue()) {
headerList.add(new Header(entry.getKey(), value));
}
}
}
return headerList;
}
/**
* Checks if a response message contains a body.
*
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3">RFC 7230 section 3.3</a>
* @param requestMethod request method
* @param responseCode response status code
* @return whether the response has a body
*/
private static boolean hasResponseBody(int requestMethod, int responseCode) {
return requestMethod != Request.Method.HEAD
&& !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK)
&& responseCode != HttpURLConnection.HTTP_NO_CONTENT
&& responseCode != HttpURLConnection.HTTP_NOT_MODIFIED;
}
/**
* Wrapper for a {@link HttpURLConnection}'s InputStream which disconnects the connection on
* stream close.
*/
static class UrlConnectionInputStream extends FilterInputStream {
private final HttpURLConnection mConnection;
UrlConnectionInputStream(HttpURLConnection connection) {
super(inputStreamFromConnection(connection));
mConnection = connection;
}
@Override
public void close() throws IOException {
super.close();
mConnection.disconnect();
}
}
/**
* Create and return an InputStream from which the response will be read.
*
* <p>May be overridden by subclasses to manipulate or monitor this input stream.
*
* @param request current request.
* @param connection current connection of request.
* @return an InputStream from which the response will be read.
*/
protected InputStream createInputStream(Request<?> request, HttpURLConnection connection) {
return new UrlConnectionInputStream(connection);
}
/**
* Initializes an {@link InputStream} from the given {@link HttpURLConnection}.
*
* @param connection
* @return an HttpEntity populated with data from <code>connection</code>.
*/
private static InputStream inputStreamFromConnection(HttpURLConnection connection) {
InputStream inputStream;
try {
inputStream = connection.getInputStream();
} catch (IOException ioe) {
inputStream = connection.getErrorStream();
}
return inputStream;
}
/** Create an {@link HttpURLConnection} for the specified {@code url}. */
protected HttpURLConnection createConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Workaround for the M release HttpURLConnection not observing the
// HttpURLConnection.setFollowRedirects() property.
// https://code.google.com/p/android/issues/detail?id=194495
connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects());
return connection;
}
/**
* Opens an {@link HttpURLConnection} with parameters.
*
* @param url
* @return an open connection
* @throws IOException
*/
private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
HttpURLConnection connection = createConnection(url);
int timeoutMs = request.getTimeoutMs();
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);
connection.setUseCaches(false);
connection.setDoInput(true);
// use caller-provided custom SslSocketFactory, if any, for HTTPS
if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory);
}
return connection;
}
// NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be
// checked against the existing properties in the connection and not overridden if already set.
@SuppressWarnings("deprecation")
/* package */ void setConnectionParametersForRequest(
HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError {
switch (request.getMethod()) {
case Method.DEPRECATED_GET_OR_POST:
// This is the deprecated way that needs to be handled for backwards compatibility.
// If the request's post body is null, then the assumption is that the request is
// GET. Otherwise, it is assumed that the request is a POST.
byte[] postBody = request.getPostBody();
if (postBody != null) {
connection.setRequestMethod("POST");
addBody(connection, request, postBody);
}
break;
case Method.GET:
// Not necessary to set the request method because connection defaults to GET but
// being explicit here.
connection.setRequestMethod("GET");
break;
case Method.DELETE:
connection.setRequestMethod("DELETE");
break;
case Method.POST:
connection.setRequestMethod("POST");
addBodyIfExists(connection, request);
break;
case Method.PUT:
connection.setRequestMethod("PUT");
addBodyIfExists(connection, request);
break;
case Method.HEAD:
connection.setRequestMethod("HEAD");
break;
case Method.OPTIONS:
connection.setRequestMethod("OPTIONS");
break;
case Method.TRACE:
connection.setRequestMethod("TRACE");
break;
case Method.PATCH:
connection.setRequestMethod("PATCH");
addBodyIfExists(connection, request);
break;
default:
throw new IllegalStateException("Unknown method type.");
}
}
private void addBodyIfExists(HttpURLConnection connection, Request<?> request)
throws IOException, AuthFailureError {
byte[] body = request.getBody();
if (body != null) {
addBody(connection, request, body);
}
}
private void addBody(HttpURLConnection connection, Request<?> request, byte[] body)
throws IOException {
// Prepare output. There is no need to set Content-Length explicitly,
// since this is handled by HttpURLConnection using the size of the prepared
// output stream.
connection.setDoOutput(true);
// Set the content-type unless it was already set (by Request#getHeaders).
if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) {
connection.setRequestProperty(
HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType());
}
DataOutputStream out =
new DataOutputStream(createOutputStream(request, connection, body.length));
out.write(body);
out.close();
}
/**
* Create and return an OutputStream to which the request body will be written.
*
* <p>May be overridden by subclasses to manipulate or monitor this output stream.
*
* @param request current request.
* @param connection current connection of request.
* @param length size of stream to write.
* @return an OutputStream to which the request body will be written.
* @throws IOException if an I/O error occurs while creating the stream.
*/
protected OutputStream createOutputStream(
Request<?> request, HttpURLConnection connection, int length) throws IOException {
return connection.getOutputStream();
}
}