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