| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 libcore.net.http; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.Authenticator; |
| import java.net.HttpRetryException; |
| import java.net.HttpURLConnection; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.PasswordAuthentication; |
| import java.net.ProtocolException; |
| import java.net.Proxy; |
| import java.net.SocketPermission; |
| import java.net.URL; |
| import java.nio.charset.Charsets; |
| import java.security.Permission; |
| import java.util.List; |
| import java.util.Map; |
| import libcore.io.Base64; |
| import libcore.io.IoUtils; |
| |
| /** |
| * This implementation uses HttpEngine to send requests and receive responses. |
| * This class may use multiple HttpEngines to follow redirects, authentication |
| * retries, etc. to retrieve the final response body. |
| * |
| * <h3>What does 'connected' mean?</h3> |
| * This class inherits a {@code connected} field from the superclass. That field |
| * is <strong>not</strong> used to indicate not whether this URLConnection is |
| * currently connected. Instead, it indicates whether a connection has ever been |
| * attempted. Once a connection has been attempted, certain properties (request |
| * header fields, request method, etc.) are immutable. Test the {@code |
| * connection} field on this class for null/non-null to determine of an instance |
| * is currently connected to a server. |
| */ |
| class HttpURLConnectionImpl extends HttpURLConnection { |
| |
| private final int defaultPort; |
| |
| private Proxy proxy; |
| |
| private final RawHeaders rawRequestHeaders = new RawHeaders(); |
| |
| private int redirectionCount; |
| |
| protected IOException httpEngineFailure; |
| protected HttpEngine httpEngine; |
| |
| protected HttpURLConnectionImpl(URL url, int port) { |
| super(url); |
| defaultPort = port; |
| } |
| |
| protected HttpURLConnectionImpl(URL url, int port, Proxy proxy) { |
| this(url, port); |
| this.proxy = proxy; |
| } |
| |
| @Override public final void connect() throws IOException { |
| initHttpEngine(); |
| try { |
| httpEngine.sendRequest(); |
| } catch (IOException e) { |
| httpEngineFailure = e; |
| throw e; |
| } |
| } |
| |
| @Override public final void disconnect() { |
| // Calling disconnect() before a connection exists should have no effect. |
| if (httpEngine != null) { |
| // We close the response body here instead of in |
| // HttpEngine.release because that is called when input |
| // has been completely read from the underlying socket. |
| // However the response body can be a GZIPInputStream that |
| // still has unread data. |
| if (httpEngine.hasResponse()) { |
| IoUtils.closeQuietly(httpEngine.getResponseBody()); |
| } |
| httpEngine.release(false); |
| } |
| } |
| |
| /** |
| * Returns an input stream from the server in the case of error such as the |
| * requested file (txt, htm, html) is not found on the remote server. |
| */ |
| @Override public final InputStream getErrorStream() { |
| try { |
| HttpEngine response = getResponse(); |
| if (response.hasResponseBody() |
| && response.getResponseCode() >= HTTP_BAD_REQUEST) { |
| return response.getResponseBody(); |
| } |
| return null; |
| } catch (IOException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the value of the field at {@code position}. Returns null if there |
| * are fewer than {@code position} headers. |
| */ |
| @Override public final String getHeaderField(int position) { |
| try { |
| return getResponse().getResponseHeaders().getHeaders().getValue(position); |
| } catch (IOException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the value of the field corresponding to the {@code fieldName}, or |
| * null if there is no such field. If the field has multiple values, the |
| * last value is returned. |
| */ |
| @Override public final String getHeaderField(String fieldName) { |
| try { |
| RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders(); |
| return fieldName == null |
| ? rawHeaders.getStatusLine() |
| : rawHeaders.get(fieldName); |
| } catch (IOException e) { |
| return null; |
| } |
| } |
| |
| @Override public final String getHeaderFieldKey(int position) { |
| try { |
| return getResponse().getResponseHeaders().getHeaders().getFieldName(position); |
| } catch (IOException e) { |
| return null; |
| } |
| } |
| |
| @Override public final Map<String, List<String>> getHeaderFields() { |
| try { |
| return getResponse().getResponseHeaders().getHeaders().toMultimap(); |
| } catch (IOException e) { |
| return null; |
| } |
| } |
| |
| @Override public final Map<String, List<String>> getRequestProperties() { |
| if (connected) { |
| throw new IllegalStateException( |
| "Cannot access request header fields after connection is set"); |
| } |
| return rawRequestHeaders.toMultimap(); |
| } |
| |
| @Override public final InputStream getInputStream() throws IOException { |
| if (!doInput) { |
| throw new ProtocolException("This protocol does not support input"); |
| } |
| |
| HttpEngine response = getResponse(); |
| |
| /* |
| * if the requested file does not exist, throw an exception formerly the |
| * Error page from the server was returned if the requested file was |
| * text/html this has changed to return FileNotFoundException for all |
| * file types |
| */ |
| if (getResponseCode() >= HTTP_BAD_REQUEST) { |
| throw new FileNotFoundException(url.toString()); |
| } |
| |
| InputStream result = response.getResponseBody(); |
| if (result == null) { |
| throw new IOException("No response body exists; responseCode=" + getResponseCode()); |
| } |
| return result; |
| } |
| |
| @Override public final OutputStream getOutputStream() throws IOException { |
| connect(); |
| |
| OutputStream result = httpEngine.getRequestBody(); |
| if (result == null) { |
| throw new ProtocolException("method does not support a request body: " + method); |
| } else if (httpEngine.hasResponse()) { |
| throw new ProtocolException("cannot write request body after response has been read"); |
| } |
| |
| return result; |
| } |
| |
| @Override public final Permission getPermission() throws IOException { |
| String connectToAddress = getConnectToHost() + ":" + getConnectToPort(); |
| return new SocketPermission(connectToAddress, "connect, resolve"); |
| } |
| |
| private String getConnectToHost() { |
| return usingProxy() |
| ? ((InetSocketAddress) proxy.address()).getHostName() |
| : getURL().getHost(); |
| } |
| |
| private int getConnectToPort() { |
| int hostPort = usingProxy() |
| ? ((InetSocketAddress) proxy.address()).getPort() |
| : getURL().getPort(); |
| return hostPort < 0 ? getDefaultPort() : hostPort; |
| } |
| |
| @Override public final String getRequestProperty(String field) { |
| if (field == null) { |
| return null; |
| } |
| return rawRequestHeaders.get(field); |
| } |
| |
| private void initHttpEngine() throws IOException { |
| if (httpEngineFailure != null) { |
| throw httpEngineFailure; |
| } else if (httpEngine != null) { |
| return; |
| } |
| |
| connected = true; |
| try { |
| if (doOutput) { |
| if (method == HttpEngine.GET) { |
| // they are requesting a stream to write to. This implies a POST method |
| method = HttpEngine.POST; |
| } else if (method != HttpEngine.POST && method != HttpEngine.PUT) { |
| // If the request method is neither POST nor PUT, then you're not writing |
| throw new ProtocolException(method + " does not support writing"); |
| } |
| } |
| httpEngine = newHttpEngine(method, rawRequestHeaders, null, null); |
| } catch (IOException e) { |
| httpEngineFailure = e; |
| throw e; |
| } |
| } |
| |
| /** |
| * Create a new HTTP engine. This hook method is non-final so it can be |
| * overridden by HttpsURLConnectionImpl. |
| */ |
| protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, |
| HttpConnection connection, RetryableOutputStream requestBody) throws IOException { |
| return new HttpEngine(this, method, requestHeaders, connection, requestBody); |
| } |
| |
| /** |
| * Aggressively tries to get the final HTTP response, potentially making |
| * many HTTP requests in the process in order to cope with redirects and |
| * authentication. |
| */ |
| private HttpEngine getResponse() throws IOException { |
| initHttpEngine(); |
| |
| if (httpEngine.hasResponse()) { |
| return httpEngine; |
| } |
| |
| while (true) { |
| try { |
| httpEngine.sendRequest(); |
| httpEngine.readResponse(); |
| } catch (IOException e) { |
| /* |
| * If the connection was recycled, its staleness may have caused |
| * the failure. Silently retry with a different connection. |
| */ |
| OutputStream requestBody = httpEngine.getRequestBody(); |
| if (httpEngine.hasRecycledConnection() |
| && (requestBody == null || requestBody instanceof RetryableOutputStream)) { |
| httpEngine.release(false); |
| httpEngine = newHttpEngine(method, rawRequestHeaders, null, |
| (RetryableOutputStream) requestBody); |
| continue; |
| } |
| httpEngineFailure = e; |
| throw e; |
| } |
| |
| Retry retry = processResponseHeaders(); |
| if (retry == Retry.NONE) { |
| httpEngine.automaticallyReleaseConnectionToPool(); |
| return httpEngine; |
| } |
| |
| /* |
| * The first request was insufficient. Prepare for another... |
| */ |
| String retryMethod = method; |
| OutputStream requestBody = httpEngine.getRequestBody(); |
| |
| /* |
| * Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM |
| * redirect should keep the same method, Chrome, Firefox and the |
| * RI all issue GETs when following any redirect. |
| */ |
| int responseCode = getResponseCode(); |
| if (responseCode == HTTP_MULT_CHOICE || responseCode == HTTP_MOVED_PERM |
| || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER) { |
| retryMethod = HttpEngine.GET; |
| requestBody = null; |
| } |
| |
| if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) { |
| throw new HttpRetryException("Cannot retry streamed HTTP body", |
| httpEngine.getResponseCode()); |
| } |
| |
| if (retry == Retry.DIFFERENT_CONNECTION) { |
| httpEngine.automaticallyReleaseConnectionToPool(); |
| } else { |
| httpEngine.markConnectionAsRecycled(); |
| } |
| |
| httpEngine.release(true); |
| |
| httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, |
| httpEngine.getConnection(), (RetryableOutputStream) requestBody); |
| } |
| } |
| |
| HttpEngine getHttpEngine() { |
| return httpEngine; |
| } |
| |
| enum Retry { |
| NONE, |
| SAME_CONNECTION, |
| DIFFERENT_CONNECTION |
| } |
| |
| /** |
| * Returns the retry action to take for the current response headers. The |
| * headers, proxy and target URL or this connection may be adjusted to |
| * prepare for a follow up request. |
| */ |
| private Retry processResponseHeaders() throws IOException { |
| switch (getResponseCode()) { |
| case HTTP_PROXY_AUTH: |
| if (!usingProxy()) { |
| throw new IOException( |
| "Received HTTP_PROXY_AUTH (407) code while not using proxy"); |
| } |
| // fall-through |
| case HTTP_UNAUTHORIZED: |
| boolean credentialsFound = processAuthHeader(getResponseCode(), |
| httpEngine.getResponseHeaders(), rawRequestHeaders); |
| return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE; |
| |
| case HTTP_MULT_CHOICE: |
| case HTTP_MOVED_PERM: |
| case HTTP_MOVED_TEMP: |
| case HTTP_SEE_OTHER: |
| if (!getInstanceFollowRedirects()) { |
| return Retry.NONE; |
| } |
| if (++redirectionCount > HttpEngine.MAX_REDIRECTS) { |
| throw new ProtocolException("Too many redirects"); |
| } |
| String location = getHeaderField("Location"); |
| if (location == null) { |
| return Retry.NONE; |
| } |
| URL previousUrl = url; |
| url = new URL(previousUrl, location); |
| if (!previousUrl.getProtocol().equals(url.getProtocol())) { |
| return Retry.NONE; // the scheme changed; don't retry. |
| } |
| if (previousUrl.getHost().equals(url.getHost()) |
| && previousUrl.getEffectivePort() == url.getEffectivePort()) { |
| return Retry.SAME_CONNECTION; |
| } else { |
| return Retry.DIFFERENT_CONNECTION; |
| } |
| |
| default: |
| return Retry.NONE; |
| } |
| } |
| |
| /** |
| * React to a failed authorization response by looking up new credentials. |
| * |
| * @return true if credentials have been added to successorRequestHeaders |
| * and another request should be attempted. |
| */ |
| final boolean processAuthHeader(int responseCode, ResponseHeaders response, |
| RawHeaders successorRequestHeaders) throws IOException { |
| if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) { |
| throw new IllegalArgumentException("Bad response code: " + responseCode); |
| } |
| |
| // keep asking for username/password until authorized |
| String challengeHeader = responseCode == HTTP_PROXY_AUTH |
| ? "Proxy-Authenticate" |
| : "WWW-Authenticate"; |
| String credentials = getAuthorizationCredentials(response.getHeaders(), challengeHeader); |
| if (credentials == null) { |
| return false; // could not find credentials, end request cycle |
| } |
| |
| // add authorization credentials, bypassing the already-connected check |
| String fieldName = responseCode == HTTP_PROXY_AUTH |
| ? "Proxy-Authorization" |
| : "Authorization"; |
| successorRequestHeaders.set(fieldName, credentials); |
| return true; |
| } |
| |
| /** |
| * Returns the authorization credentials on the base of provided challenge. |
| */ |
| private String getAuthorizationCredentials(RawHeaders responseHeaders, String challengeHeader) |
| throws IOException { |
| List<Challenge> challenges = HeaderParser.parseChallenges(responseHeaders, challengeHeader); |
| if (challenges.isEmpty()) { |
| throw new IOException("No authentication challenges found"); |
| } |
| |
| for (Challenge challenge : challenges) { |
| // use the global authenticator to get the password |
| PasswordAuthentication auth = Authenticator.requestPasswordAuthentication( |
| getConnectToInetAddress(), getConnectToPort(), url.getProtocol(), |
| challenge.realm, challenge.scheme); |
| if (auth == null) { |
| continue; |
| } |
| |
| // base64 encode the username and password |
| String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword()); |
| byte[] bytes = usernameAndPassword.getBytes(Charsets.ISO_8859_1); |
| String encoded = Base64.encode(bytes); |
| return challenge.scheme + " " + encoded; |
| } |
| |
| return null; |
| } |
| |
| private InetAddress getConnectToInetAddress() throws IOException { |
| return usingProxy() |
| ? ((InetSocketAddress) proxy.address()).getAddress() |
| : InetAddress.getByName(getURL().getHost()); |
| } |
| |
| final int getDefaultPort() { |
| return defaultPort; |
| } |
| |
| /** @see HttpURLConnection#setFixedLengthStreamingMode(int) */ |
| final int getFixedContentLength() { |
| return fixedContentLength; |
| } |
| |
| /** @see HttpURLConnection#setChunkedStreamingMode(int) */ |
| final int getChunkLength() { |
| return chunkLength; |
| } |
| |
| final Proxy getProxy() { |
| return proxy; |
| } |
| |
| final void setProxy(Proxy proxy) { |
| this.proxy = proxy; |
| } |
| |
| @Override public final boolean usingProxy() { |
| return (proxy != null && proxy.type() != Proxy.Type.DIRECT); |
| } |
| |
| @Override public String getResponseMessage() throws IOException { |
| return getResponse().getResponseHeaders().getHeaders().getResponseMessage(); |
| } |
| |
| @Override public final int getResponseCode() throws IOException { |
| return getResponse().getResponseCode(); |
| } |
| |
| @Override public final void setRequestProperty(String field, String newValue) { |
| if (connected) { |
| throw new IllegalStateException("Cannot set request property after connection is made"); |
| } |
| if (field == null) { |
| throw new NullPointerException("field == null"); |
| } |
| rawRequestHeaders.set(field, newValue); |
| } |
| |
| @Override public final void addRequestProperty(String field, String value) { |
| if (connected) { |
| throw new IllegalStateException("Cannot add request property after connection is made"); |
| } |
| if (field == null) { |
| throw new NullPointerException("field == null"); |
| } |
| rawRequestHeaders.add(field, value); |
| } |
| } |