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 com.android.volley.AuthFailureError;
20 import com.android.volley.Request;
21 import com.android.volley.Request.Method;
22 
23 import org.apache.http.Header;
24 import org.apache.http.HttpEntity;
25 import org.apache.http.HttpResponse;
26 import org.apache.http.HttpStatus;
27 import org.apache.http.ProtocolVersion;
28 import org.apache.http.StatusLine;
29 import org.apache.http.entity.BasicHttpEntity;
30 import org.apache.http.message.BasicHeader;
31 import org.apache.http.message.BasicHttpResponse;
32 import org.apache.http.message.BasicStatusLine;
33 
34 import java.io.DataOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.net.HttpURLConnection;
38 import java.net.URL;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 
44 import javax.net.ssl.HttpsURLConnection;
45 import javax.net.ssl.SSLSocketFactory;
46 
47 /**
48  * An {@link HttpStack} based on {@link HttpURLConnection}.
49  */
50 public class HurlStack implements HttpStack {
51 
52     private static final String HEADER_CONTENT_TYPE = "Content-Type";
53 
54     /**
55      * An interface for transforming URLs before use.
56      */
57     public interface UrlRewriter {
58         /**
59          * Returns a URL to use instead of the provided one, or null to indicate
60          * this URL should not be used at all.
61          */
rewriteUrl(String originalUrl)62         public String rewriteUrl(String originalUrl);
63     }
64 
65     private final UrlRewriter mUrlRewriter;
66     private final SSLSocketFactory mSslSocketFactory;
67 
HurlStack()68     public HurlStack() {
69         this(null);
70     }
71 
72     /**
73      * @param urlRewriter Rewriter to use for request URLs
74      */
HurlStack(UrlRewriter urlRewriter)75     public HurlStack(UrlRewriter urlRewriter) {
76         this(urlRewriter, null);
77     }
78 
79     /**
80      * @param urlRewriter Rewriter to use for request URLs
81      * @param sslSocketFactory SSL factory to use for HTTPS connections
82      */
HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory)83     public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
84         mUrlRewriter = urlRewriter;
85         mSslSocketFactory = sslSocketFactory;
86     }
87 
88     @Override
performRequest(Request<?> request, Map<String, String> additionalHeaders)89     public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
90             throws IOException, AuthFailureError {
91         String url = request.getUrl();
92         HashMap<String, String> map = new HashMap<String, String>();
93         map.putAll(request.getHeaders());
94         map.putAll(additionalHeaders);
95         if (mUrlRewriter != null) {
96             String rewritten = mUrlRewriter.rewriteUrl(url);
97             if (rewritten == null) {
98                 throw new IOException("URL blocked by rewriter: " + url);
99             }
100             url = rewritten;
101         }
102         URL parsedUrl = new URL(url);
103         HttpURLConnection connection = openConnection(parsedUrl, request);
104         for (String headerName : map.keySet()) {
105             connection.addRequestProperty(headerName, map.get(headerName));
106         }
107         setConnectionParametersForRequest(connection, request);
108         // Initialize HttpResponse with data from the HttpURLConnection.
109         ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
110         int responseCode = connection.getResponseCode();
111         if (responseCode == -1) {
112             // -1 is returned by getResponseCode() if the response code could not be retrieved.
113             // Signal to the caller that something was wrong with the connection.
114             throw new IOException("Could not retrieve response code from HttpUrlConnection.");
115         }
116         StatusLine responseStatus = new BasicStatusLine(protocolVersion,
117                 connection.getResponseCode(), connection.getResponseMessage());
118         BasicHttpResponse response = new BasicHttpResponse(responseStatus);
119         if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) {
120             response.setEntity(entityFromConnection(connection));
121         }
122         for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
123             if (header.getKey() != null) {
124                 Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
125                 response.addHeader(h);
126             }
127         }
128         return response;
129     }
130 
131     /**
132      * Checks if a response message contains a body.
133      * @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3">RFC 7230 section 3.3</a>
134      * @param requestMethod request method
135      * @param responseCode response status code
136      * @return whether the response has a body
137      */
hasResponseBody(int requestMethod, int responseCode)138     private static boolean hasResponseBody(int requestMethod, int responseCode) {
139         return requestMethod != Request.Method.HEAD
140             && !(HttpStatus.SC_CONTINUE <= responseCode && responseCode < HttpStatus.SC_OK)
141             && responseCode != HttpStatus.SC_NO_CONTENT
142             && responseCode != HttpStatus.SC_NOT_MODIFIED;
143     }
144 
145     /**
146      * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
147      * @param connection
148      * @return an HttpEntity populated with data from <code>connection</code>.
149      */
entityFromConnection(HttpURLConnection connection)150     private static HttpEntity entityFromConnection(HttpURLConnection connection) {
151         BasicHttpEntity entity = new BasicHttpEntity();
152         InputStream inputStream;
153         try {
154             inputStream = connection.getInputStream();
155         } catch (IOException ioe) {
156             inputStream = connection.getErrorStream();
157         }
158         entity.setContent(inputStream);
159         entity.setContentLength(connection.getContentLength());
160         entity.setContentEncoding(connection.getContentEncoding());
161         entity.setContentType(connection.getContentType());
162         return entity;
163     }
164 
165     /**
166      * Create an {@link HttpURLConnection} for the specified {@code url}.
167      */
createConnection(URL url)168     protected HttpURLConnection createConnection(URL url) throws IOException {
169         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
170 
171         // Workaround for the M release HttpURLConnection not observing the
172         // HttpURLConnection.setFollowRedirects() property.
173         // https://code.google.com/p/android/issues/detail?id=194495
174         connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects());
175 
176         return connection;
177     }
178 
179     /**
180      * Opens an {@link HttpURLConnection} with parameters.
181      * @param url
182      * @return an open connection
183      * @throws IOException
184      */
openConnection(URL url, Request<?> request)185     private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
186         HttpURLConnection connection = createConnection(url);
187 
188         int timeoutMs = request.getTimeoutMs();
189         connection.setConnectTimeout(timeoutMs);
190         connection.setReadTimeout(timeoutMs);
191         connection.setUseCaches(false);
192         connection.setDoInput(true);
193 
194         // use caller-provided custom SslSocketFactory, if any, for HTTPS
195         if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
196             ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
197         }
198 
199         return connection;
200     }
201 
202     @SuppressWarnings("deprecation")
setConnectionParametersForRequest(HttpURLConnection connection, Request<?> request)203     /* package */ static void setConnectionParametersForRequest(HttpURLConnection connection,
204             Request<?> request) throws IOException, AuthFailureError {
205         switch (request.getMethod()) {
206             case Method.DEPRECATED_GET_OR_POST:
207                 // This is the deprecated way that needs to be handled for backwards compatibility.
208                 // If the request's post body is null, then the assumption is that the request is
209                 // GET.  Otherwise, it is assumed that the request is a POST.
210                 byte[] postBody = request.getPostBody();
211                 if (postBody != null) {
212                     // Prepare output. There is no need to set Content-Length explicitly,
213                     // since this is handled by HttpURLConnection using the size of the prepared
214                     // output stream.
215                     connection.setDoOutput(true);
216                     connection.setRequestMethod("POST");
217                     connection.addRequestProperty(HEADER_CONTENT_TYPE,
218                             request.getPostBodyContentType());
219                     DataOutputStream out = new DataOutputStream(connection.getOutputStream());
220                     out.write(postBody);
221                     out.close();
222                 }
223                 break;
224             case Method.GET:
225                 // Not necessary to set the request method because connection defaults to GET but
226                 // being explicit here.
227                 connection.setRequestMethod("GET");
228                 break;
229             case Method.DELETE:
230                 connection.setRequestMethod("DELETE");
231                 break;
232             case Method.POST:
233                 connection.setRequestMethod("POST");
234                 addBodyIfExists(connection, request);
235                 break;
236             case Method.PUT:
237                 connection.setRequestMethod("PUT");
238                 addBodyIfExists(connection, request);
239                 break;
240             case Method.HEAD:
241                 connection.setRequestMethod("HEAD");
242                 break;
243             case Method.OPTIONS:
244                 connection.setRequestMethod("OPTIONS");
245                 break;
246             case Method.TRACE:
247                 connection.setRequestMethod("TRACE");
248                 break;
249             case Method.PATCH:
250                 connection.setRequestMethod("PATCH");
251                 addBodyIfExists(connection, request);
252                 break;
253             default:
254                 throw new IllegalStateException("Unknown method type.");
255         }
256     }
257 
addBodyIfExists(HttpURLConnection connection, Request<?> request)258     private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
259             throws IOException, AuthFailureError {
260         byte[] body = request.getBody();
261         if (body != null) {
262             connection.setDoOutput(true);
263             connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
264             DataOutputStream out = new DataOutputStream(connection.getOutputStream());
265             out.write(body);
266             out.close();
267         }
268     }
269 }
270