aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/com/android/volley/toolbox/HurlStack.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main/java/com/android/volley/toolbox/HurlStack.java')
-rw-r--r--core/src/main/java/com/android/volley/toolbox/HurlStack.java321
1 files changed, 321 insertions, 0 deletions
diff --git a/core/src/main/java/com/android/volley/toolbox/HurlStack.java b/core/src/main/java/com/android/volley/toolbox/HurlStack.java
new file mode 100644
index 0000000..35c6a72
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -0,0 +1,321 @@
+/*
+ * 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();
+ }
+}