blob: 32c801f96b93539fe886daddc67840d526fb041e [file] [log] [blame]
/*
* 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 org.apache.harmony.luni.internal.net.www.protocol.http;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import libcore.base.Objects;
import org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl;
/**
* Holds the sockets and streams of an HTTP or HTTPS connection, which may be
* used for multiple HTTP request/response exchanges. Connections may be direct
* to the origin server or via a proxy. Create an instance using the {@link
* Address} inner class.
*
* <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
* which isn't so much a connection as a single request/response pair.
*/
public final class HttpConnection {
private final Address address;
private final Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
private SSLSocket unverifiedSocket;
private SSLSocket sslSocket;
private InputStream sslInputStream;
private OutputStream sslOutputStream;
private HttpConnection(Address config, int connectTimeout) throws IOException {
this.address = config;
/*
* Try each of the host's addresses for best behaviour in mixed IPv4/IPv6
* environments. See http://b/2876927
* TODO: add a hidden method so that Socket.tryAllAddresses can does this for us
*/
Socket socketCandidate = null;
InetAddress[] addresses = InetAddress.getAllByName(config.socketHost);
for (int i = 0; i < addresses.length; i++) {
socketCandidate = (config.proxy != null && config.proxy.type() != Proxy.Type.HTTP)
? new Socket(config.proxy)
: new Socket();
try {
socketCandidate.connect(
new InetSocketAddress(addresses[i], config.socketPort), connectTimeout);
break;
} catch (IOException e) {
if (i == addresses.length - 1) {
throw e;
}
}
}
this.socket = socketCandidate;
}
public void closeSocketAndStreams() {
closeQuietly(sslOutputStream);
closeQuietly(sslInputStream);
closeQuietly(sslSocket);
closeQuietly(outputStream);
closeQuietly(inputStream);
closeQuietly(socket);
}
private void closeQuietly(Socket socket) {
if (socket != null) {
try {
socket.close();
} catch (Exception ignored) {
}
}
}
private void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception ignored) {
}
}
}
public void setSoTimeout(int readTimeout) throws SocketException {
socket.setSoTimeout(readTimeout);
}
public OutputStream getOutputStream() throws IOException {
if (sslSocket != null) {
if (sslOutputStream == null) {
sslOutputStream = sslSocket.getOutputStream();
}
return sslOutputStream;
} else if(outputStream == null) {
outputStream = socket.getOutputStream();
}
return outputStream;
}
public InputStream getInputStream() throws IOException {
if (sslSocket != null) {
if (sslInputStream == null) {
sslInputStream = sslSocket.getInputStream();
}
return sslInputStream;
} else if (inputStream == null) {
/*
* Buffer the socket stream to permit efficient parsing of HTTP
* headers and chunk sizes. Benchmarks suggest 128 is sufficient.
* We cannot buffer when setting up a tunnel because we may consume
* bytes intended for the SSL socket.
*/
int bufferSize = 128;
inputStream = address.requiresTunnel
? socket.getInputStream()
: new BufferedInputStream(socket.getInputStream(), bufferSize);
}
return inputStream;
}
private Socket getSocket() {
return sslSocket != null ? sslSocket : socket;
}
public Address getAddress() {
return address;
}
/**
* Create an {@code SSLSocket} and perform the SSL handshake
* (performing certificate validation.
*
* @param sslSocketFactory Source of new {@code SSLSocket} instances.
* @param tlsTolerant If true, assume server can handle common
* TLS extensions and SSL deflate compression. If false, use
* an SSL3 only fallback mode without compression.
*/
public void setupSecureSocket(SSLSocketFactory sslSocketFactory, boolean tlsTolerant)
throws IOException {
// create the wrapper over connected socket
unverifiedSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
address.uriHost, address.uriPort, true /* autoClose */);
// tlsTolerant mimics Chrome's behavior
if (tlsTolerant && unverifiedSocket instanceof OpenSSLSocketImpl) {
OpenSSLSocketImpl openSslSocket = (OpenSSLSocketImpl) unverifiedSocket;
openSslSocket.setEnabledCompressionMethods(new String[] { "ZLIB"});
openSslSocket.setUseSessionTickets(true);
openSslSocket.setHostname(address.socketHost);
// use SSLSocketFactory default enabled protocols
} else {
unverifiedSocket.setEnabledProtocols(new String [] { "SSLv3" });
}
// force handshake, which can throw
unverifiedSocket.startHandshake();
}
/**
* Return an {@code SSLSocket} that is not only connected but has
* also passed hostname verification.
*
* @param hostnameVerifier Used to verify the hostname we
* connected to is an acceptable match for the peer certificate
* chain of the SSLSession.
*/
public SSLSocket verifySecureSocketHostname(HostnameVerifier hostnameVerifier)
throws IOException {
if (!hostnameVerifier.verify(address.uriHost, unverifiedSocket.getSession())) {
throw new IOException("Hostname '" + address.uriHost + "' was not verified");
}
sslSocket = unverifiedSocket;
return sslSocket;
}
/**
* Return an {@code SSLSocket} if already connected, otherwise null.
*/
public SSLSocket getSecureSocketIfConnected() {
return sslSocket;
}
/**
* Returns true if the connection is functional. This uses a shameful hack
* to peek a byte from the socket.
*/
boolean isStale() throws IOException {
if (!isEligibleForRecycling()) {
return true;
}
InputStream in = getInputStream();
if (in.available() > 0) {
return false;
}
Socket socket = getSocket();
int soTimeout = socket.getSoTimeout();
try {
socket.setSoTimeout(1);
in.mark(1);
int byteRead = in.read();
if (byteRead != -1) {
in.reset();
return false;
}
return true; // the socket is reporting all data read; it's stale
} catch (SocketTimeoutException e) {
return false; // the connection is not stale; hooray
} catch (IOException e) {
return true; // the connection is stale, the read or soTimeout failed.
} finally {
socket.setSoTimeout(soTimeout);
}
}
/**
* Returns true if this connection is eligible to be recycled. This
* is like {@link #isStale} except that it doesn't try to actually
* perform any I/O.
*/
protected boolean isEligibleForRecycling() {
return !socket.isClosed()
&& !socket.isInputShutdown()
&& !socket.isOutputShutdown();
}
/**
* This address has two parts: the address we connect to directly and the
* origin address of the resource. These are the same unless a proxy is
* being used.
*/
public static final class Address {
private final Proxy proxy;
private final boolean requiresTunnel;
private final String uriHost;
private final int uriPort;
private final String socketHost;
private final int socketPort;
public Address(URI uri) {
this.proxy = null;
this.requiresTunnel = false;
this.uriHost = uri.getHost();
this.uriPort = uri.getEffectivePort();
this.socketHost = uriHost;
this.socketPort = uriPort;
}
/**
* @param requiresTunnel true if the HTTP connection needs to tunnel one
* protocol over another, such as when using HTTPS through an HTTP
* proxy. When doing so, we must avoid buffering bytes intended for
* the higher-level protocol.
*/
public Address(URI uri, Proxy proxy, boolean requiresTunnel) {
this.proxy = proxy;
this.requiresTunnel = requiresTunnel;
this.uriHost = uri.getHost();
this.uriPort = uri.getEffectivePort();
SocketAddress proxyAddress = proxy.address();
if (!(proxyAddress instanceof InetSocketAddress)) {
throw new IllegalArgumentException("Proxy.address() is not an InetSocketAddress: "
+ proxyAddress.getClass());
}
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
this.socketHost = proxySocketAddress.getHostName();
this.socketPort = proxySocketAddress.getPort();
}
@Override public boolean equals(Object other) {
if (other instanceof Address) {
Address that = (Address) other;
return Objects.equal(this.proxy, that.proxy)
&& this.uriHost.equals(that.uriHost)
&& this.uriPort == that.uriPort
&& this.requiresTunnel == that.requiresTunnel;
}
return false;
}
@Override public int hashCode() {
int result = 17;
result = 31 * result + uriHost.hashCode();
result = 31 * result + uriPort;
result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
result = 31 * result + (requiresTunnel ? 1 : 0);
return result;
}
public HttpConnection connect(int connectTimeout) throws IOException {
return new HttpConnection(this, connectTimeout);
}
}
}