• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */
17 
18 package libcore.net.http;
19 
20 import java.io.FileNotFoundException;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.net.Authenticator;
25 import java.net.HttpRetryException;
26 import java.net.HttpURLConnection;
27 import java.net.InetAddress;
28 import java.net.InetSocketAddress;
29 import java.net.PasswordAuthentication;
30 import java.net.ProtocolException;
31 import java.net.Proxy;
32 import java.net.SocketPermission;
33 import java.net.URL;
34 import java.nio.charset.Charsets;
35 import java.security.Permission;
36 import java.util.List;
37 import java.util.Map;
38 import libcore.io.Base64;
39 
40 /**
41  * This implementation uses HttpEngine to send requests and receive responses.
42  * This class may use multiple HttpEngines to follow redirects, authentication
43  * retries, etc. to retrieve the final response body.
44  *
45  * <h3>What does 'connected' mean?</h3>
46  * This class inherits a {@code connected} field from the superclass. That field
47  * is <strong>not</strong> used to indicate not whether this URLConnection is
48  * currently connected. Instead, it indicates whether a connection has ever been
49  * attempted. Once a connection has been attempted, certain properties (request
50  * header fields, request method, etc.) are immutable. Test the {@code
51  * connection} field on this class for null/non-null to determine of an instance
52  * is currently connected to a server.
53  */
54 class HttpURLConnectionImpl extends HttpURLConnection {
55 
56     private final int defaultPort;
57 
58     private Proxy proxy;
59 
60     private final RawHeaders rawRequestHeaders = new RawHeaders();
61 
62     private int redirectionCount;
63 
64     protected IOException httpEngineFailure;
65     protected HttpEngine httpEngine;
66 
HttpURLConnectionImpl(URL url, int port)67     protected HttpURLConnectionImpl(URL url, int port) {
68         super(url);
69         defaultPort = port;
70     }
71 
HttpURLConnectionImpl(URL url, int port, Proxy proxy)72     protected HttpURLConnectionImpl(URL url, int port, Proxy proxy) {
73         this(url, port);
74         this.proxy = proxy;
75     }
76 
connect()77     @Override public final void connect() throws IOException {
78         initHttpEngine();
79         try {
80             httpEngine.sendRequest();
81         } catch (IOException e) {
82             httpEngineFailure = e;
83             throw e;
84         }
85     }
86 
disconnect()87     @Override public final void disconnect() {
88         // Calling disconnect() before a connection exists should have no effect.
89         if (httpEngine != null) {
90             httpEngine.release(false);
91         }
92     }
93 
94     /**
95      * Returns an input stream from the server in the case of error such as the
96      * requested file (txt, htm, html) is not found on the remote server.
97      */
getErrorStream()98     @Override public final InputStream getErrorStream() {
99         try {
100             HttpEngine response = getResponse();
101             if (response.hasResponseBody()
102                     && response.getResponseCode() >= HTTP_BAD_REQUEST) {
103                 return response.getResponseBody();
104             }
105             return null;
106         } catch (IOException e) {
107             return null;
108         }
109     }
110 
111     /**
112      * Returns the value of the field at {@code position}. Returns null if there
113      * are fewer than {@code position} headers.
114      */
getHeaderField(int position)115     @Override public final String getHeaderField(int position) {
116         try {
117             return getResponse().getResponseHeaders().getHeaders().getValue(position);
118         } catch (IOException e) {
119             return null;
120         }
121     }
122 
123     /**
124      * Returns the value of the field corresponding to the {@code fieldName}, or
125      * null if there is no such field. If the field has multiple values, the
126      * last value is returned.
127      */
getHeaderField(String fieldName)128     @Override public final String getHeaderField(String fieldName) {
129         try {
130             RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
131             return fieldName == null
132                     ? rawHeaders.getStatusLine()
133                     : rawHeaders.get(fieldName);
134         } catch (IOException e) {
135             return null;
136         }
137     }
138 
getHeaderFieldKey(int position)139     @Override public final String getHeaderFieldKey(int position) {
140         try {
141             return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
142         } catch (IOException e) {
143             return null;
144         }
145     }
146 
getHeaderFields()147     @Override public final Map<String, List<String>> getHeaderFields() {
148         try {
149             return getResponse().getResponseHeaders().getHeaders().toMultimap();
150         } catch (IOException e) {
151             return null;
152         }
153     }
154 
getRequestProperties()155     @Override public final Map<String, List<String>> getRequestProperties() {
156         if (connected) {
157             throw new IllegalStateException(
158                     "Cannot access request header fields after connection is set");
159         }
160         return rawRequestHeaders.toMultimap();
161     }
162 
getInputStream()163     @Override public final InputStream getInputStream() throws IOException {
164         if (!doInput) {
165             throw new ProtocolException("This protocol does not support input");
166         }
167 
168         HttpEngine response = getResponse();
169 
170         /*
171          * if the requested file does not exist, throw an exception formerly the
172          * Error page from the server was returned if the requested file was
173          * text/html this has changed to return FileNotFoundException for all
174          * file types
175          */
176         if (getResponseCode() >= HTTP_BAD_REQUEST) {
177             throw new FileNotFoundException(url.toString());
178         }
179 
180         InputStream result = response.getResponseBody();
181         if (result == null) {
182             throw new IOException("No response body exists; responseCode=" + getResponseCode());
183         }
184         return result;
185     }
186 
getOutputStream()187     @Override public final OutputStream getOutputStream() throws IOException {
188         connect();
189 
190         OutputStream result = httpEngine.getRequestBody();
191         if (result == null) {
192             throw new ProtocolException("method does not support a request body: " + method);
193         } else if (httpEngine.hasResponse()) {
194             throw new ProtocolException("cannot write request body after response has been read");
195         }
196 
197         return result;
198     }
199 
getPermission()200     @Override public final Permission getPermission() throws IOException {
201         String connectToAddress = getConnectToHost() + ":" + getConnectToPort();
202         return new SocketPermission(connectToAddress, "connect, resolve");
203     }
204 
getConnectToHost()205     private String getConnectToHost() {
206         return usingProxy()
207                 ? ((InetSocketAddress) proxy.address()).getHostName()
208                 : getURL().getHost();
209     }
210 
getConnectToPort()211     private int getConnectToPort() {
212         int hostPort = usingProxy()
213                 ? ((InetSocketAddress) proxy.address()).getPort()
214                 : getURL().getPort();
215         return hostPort < 0 ? getDefaultPort() : hostPort;
216     }
217 
getRequestProperty(String field)218     @Override public final String getRequestProperty(String field) {
219         if (field == null) {
220             return null;
221         }
222         return rawRequestHeaders.get(field);
223     }
224 
initHttpEngine()225     private void initHttpEngine() throws IOException {
226         if (httpEngineFailure != null) {
227             throw httpEngineFailure;
228         } else if (httpEngine != null) {
229             return;
230         }
231 
232         connected = true;
233         try {
234             if (doOutput) {
235                 if (method == HttpEngine.GET) {
236                     // they are requesting a stream to write to. This implies a POST method
237                     method = HttpEngine.POST;
238                 } else if (method != HttpEngine.POST && method != HttpEngine.PUT) {
239                     // If the request method is neither POST nor PUT, then you're not writing
240                     throw new ProtocolException(method + " does not support writing");
241                 }
242             }
243             httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
244         } catch (IOException e) {
245             httpEngineFailure = e;
246             throw e;
247         }
248     }
249 
250     /**
251      * Create a new HTTP engine. This hook method is non-final so it can be
252      * overridden by HttpsURLConnectionImpl.
253      */
newHttpEngine(String method, RawHeaders requestHeaders, HttpConnection connection, RetryableOutputStream requestBody)254     protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
255             HttpConnection connection, RetryableOutputStream requestBody) throws IOException {
256         return new HttpEngine(this, method, requestHeaders, connection, requestBody);
257     }
258 
259     /**
260      * Aggressively tries to get the final HTTP response, potentially making
261      * many HTTP requests in the process in order to cope with redirects and
262      * authentication.
263      */
getResponse()264     private HttpEngine getResponse() throws IOException {
265         initHttpEngine();
266 
267         if (httpEngine.hasResponse()) {
268             return httpEngine;
269         }
270 
271         while (true) {
272             try {
273                 httpEngine.sendRequest();
274                 httpEngine.readResponse();
275             } catch (IOException e) {
276                 /*
277                  * If the connection was recycled, its staleness may have caused
278                  * the failure. Silently retry with a different connection.
279                  */
280                 OutputStream requestBody = httpEngine.getRequestBody();
281                 if (httpEngine.hasRecycledConnection()
282                         && (requestBody == null || requestBody instanceof RetryableOutputStream)) {
283                     httpEngine.release(false);
284                     httpEngine = newHttpEngine(method, rawRequestHeaders, null,
285                             (RetryableOutputStream) requestBody);
286                     continue;
287                 }
288                 httpEngineFailure = e;
289                 throw e;
290             }
291 
292             Retry retry = processResponseHeaders();
293             if (retry == Retry.NONE) {
294                 httpEngine.automaticallyReleaseConnectionToPool();
295                 return httpEngine;
296             }
297 
298             /*
299              * The first request was insufficient. Prepare for another...
300              */
301             String retryMethod = method;
302             OutputStream requestBody = httpEngine.getRequestBody();
303 
304             /*
305              * Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
306              * redirect should keep the same method, Chrome, Firefox and the
307              * RI all issue GETs when following any redirect.
308              */
309             int responseCode = getResponseCode();
310             if (responseCode == HTTP_MULT_CHOICE || responseCode == HTTP_MOVED_PERM
311                     || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER) {
312                 retryMethod = HttpEngine.GET;
313                 requestBody = null;
314             }
315 
316             if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
317                 throw new HttpRetryException("Cannot retry streamed HTTP body",
318                         httpEngine.getResponseCode());
319             }
320 
321             if (retry == Retry.DIFFERENT_CONNECTION) {
322                 httpEngine.automaticallyReleaseConnectionToPool();
323             }
324 
325             httpEngine.release(true);
326 
327             httpEngine = newHttpEngine(retryMethod, rawRequestHeaders,
328                     httpEngine.getConnection(), (RetryableOutputStream) requestBody);
329         }
330     }
331 
getHttpEngine()332     HttpEngine getHttpEngine() {
333         return httpEngine;
334     }
335 
336     enum Retry {
337         NONE,
338         SAME_CONNECTION,
339         DIFFERENT_CONNECTION
340     }
341 
342     /**
343      * Returns the retry action to take for the current response headers. The
344      * headers, proxy and target URL or this connection may be adjusted to
345      * prepare for a follow up request.
346      */
processResponseHeaders()347     private Retry processResponseHeaders() throws IOException {
348         switch (getResponseCode()) {
349         case HTTP_PROXY_AUTH:
350             if (!usingProxy()) {
351                 throw new IOException(
352                         "Received HTTP_PROXY_AUTH (407) code while not using proxy");
353             }
354             // fall-through
355         case HTTP_UNAUTHORIZED:
356             boolean credentialsFound = processAuthHeader(getResponseCode(),
357                     httpEngine.getResponseHeaders(), rawRequestHeaders);
358             return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
359 
360         case HTTP_MULT_CHOICE:
361         case HTTP_MOVED_PERM:
362         case HTTP_MOVED_TEMP:
363         case HTTP_SEE_OTHER:
364             if (!getInstanceFollowRedirects()) {
365                 return Retry.NONE;
366             }
367             if (++redirectionCount > HttpEngine.MAX_REDIRECTS) {
368                 throw new ProtocolException("Too many redirects");
369             }
370             String location = getHeaderField("Location");
371             if (location == null) {
372                 return Retry.NONE;
373             }
374             URL previousUrl = url;
375             url = new URL(previousUrl, location);
376             if (!previousUrl.getProtocol().equals(url.getProtocol())) {
377                 return Retry.NONE; // the scheme changed; don't retry.
378             }
379             if (previousUrl.getHost().equals(url.getHost())
380                     && previousUrl.getEffectivePort() == url.getEffectivePort()) {
381                 return Retry.SAME_CONNECTION;
382             } else {
383                 return Retry.DIFFERENT_CONNECTION;
384             }
385 
386         default:
387             return Retry.NONE;
388         }
389     }
390 
391     /**
392      * React to a failed authorization response by looking up new credentials.
393      *
394      * @return true if credentials have been added to successorRequestHeaders
395      *     and another request should be attempted.
396      */
processAuthHeader(int responseCode, ResponseHeaders response, RawHeaders successorRequestHeaders)397     final boolean processAuthHeader(int responseCode, ResponseHeaders response,
398             RawHeaders successorRequestHeaders) throws IOException {
399         if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
400             throw new IllegalArgumentException();
401         }
402 
403         // keep asking for username/password until authorized
404         String challengeHeader = responseCode == HTTP_PROXY_AUTH
405                 ? "Proxy-Authenticate"
406                 : "WWW-Authenticate";
407         String credentials = getAuthorizationCredentials(response.getHeaders(), challengeHeader);
408         if (credentials == null) {
409             return false; // could not find credentials, end request cycle
410         }
411 
412         // add authorization credentials, bypassing the already-connected check
413         String fieldName = responseCode == HTTP_PROXY_AUTH
414                 ? "Proxy-Authorization"
415                 : "Authorization";
416         successorRequestHeaders.set(fieldName, credentials);
417         return true;
418     }
419 
420     /**
421      * Returns the authorization credentials on the base of provided challenge.
422      */
getAuthorizationCredentials(RawHeaders responseHeaders, String challengeHeader)423     private String getAuthorizationCredentials(RawHeaders responseHeaders, String challengeHeader)
424             throws IOException {
425         List<Challenge> challenges = HeaderParser.parseChallenges(responseHeaders, challengeHeader);
426         if (challenges.isEmpty()) {
427             throw new IOException("No authentication challenges found");
428         }
429 
430         for (Challenge challenge : challenges) {
431             // use the global authenticator to get the password
432             PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(
433                     getConnectToInetAddress(), getConnectToPort(), url.getProtocol(),
434                     challenge.realm, challenge.scheme);
435             if (auth == null) {
436                 continue;
437             }
438 
439             // base64 encode the username and password
440             String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
441             byte[] bytes = usernameAndPassword.getBytes(Charsets.ISO_8859_1);
442             String encoded = Base64.encode(bytes);
443             return challenge.scheme + " " + encoded;
444         }
445 
446         return null;
447     }
448 
getConnectToInetAddress()449     private InetAddress getConnectToInetAddress() throws IOException {
450         return usingProxy()
451                 ? ((InetSocketAddress) proxy.address()).getAddress()
452                 : InetAddress.getByName(getURL().getHost());
453     }
454 
getDefaultPort()455     final int getDefaultPort() {
456         return defaultPort;
457     }
458 
459     /** @see HttpURLConnection#setFixedLengthStreamingMode(int) */
getFixedContentLength()460     final int getFixedContentLength() {
461         return fixedContentLength;
462     }
463 
464     /** @see HttpURLConnection#setChunkedStreamingMode(int) */
getChunkLength()465     final int getChunkLength() {
466         return chunkLength;
467     }
468 
getProxy()469     final Proxy getProxy() {
470         return proxy;
471     }
472 
setProxy(Proxy proxy)473     final void setProxy(Proxy proxy) {
474         this.proxy = proxy;
475     }
476 
usingProxy()477     @Override public final boolean usingProxy() {
478         return (proxy != null && proxy.type() != Proxy.Type.DIRECT);
479     }
480 
getResponseMessage()481     @Override public String getResponseMessage() throws IOException {
482         return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
483     }
484 
getResponseCode()485     @Override public final int getResponseCode() throws IOException {
486         return getResponse().getResponseCode();
487     }
488 
setRequestProperty(String field, String newValue)489     @Override public final void setRequestProperty(String field, String newValue) {
490         if (connected) {
491             throw new IllegalStateException("Cannot set request property after connection is made");
492         }
493         if (field == null) {
494             throw new NullPointerException("field == null");
495         }
496         rawRequestHeaders.set(field, newValue);
497     }
498 
addRequestProperty(String field, String value)499     @Override public final void addRequestProperty(String field, String value) {
500         if (connected) {
501             throw new IllegalStateException("Cannot add request property after connection is made");
502         }
503         if (field == null) {
504             throw new NullPointerException("field == null");
505         }
506         rawRequestHeaders.add(field, value);
507     }
508 }
509