| /* |
| * 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(); |
| } |
| } |