| /* HTTPConnection.java -- | |
| Copyright (C) 2004, 2005, 2006 Free Software Foundation, Inc. | |
| This file is part of GNU Classpath. | |
| GNU Classpath is free software; you can redistribute it and/or modify | |
| it under the terms of the GNU General Public License as published by | |
| the Free Software Foundation; either version 2, or (at your option) | |
| any later version. | |
| GNU Classpath is distributed in the hope that it will be useful, but | |
| WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| General Public License for more details. | |
| You should have received a copy of the GNU General Public License | |
| along with GNU Classpath; see the file COPYING. If not, write to the | |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| 02110-1301 USA. | |
| Linking this library statically or dynamically with other modules is | |
| making a combined work based on this library. Thus, the terms and | |
| conditions of the GNU General Public License cover the whole | |
| combination. | |
| As a special exception, the copyright holders of this library give you | |
| permission to link this library with independent modules to produce an | |
| executable, regardless of the license terms of these independent | |
| modules, and to copy and distribute the resulting executable under | |
| terms of your choice, provided that you also meet, for each linked | |
| independent module, the terms and conditions of the license of that | |
| module. An independent module is a module which is not derived from | |
| or based on this library. If you modify this library, you may extend | |
| this exception to your version of the library, but you are not | |
| obligated to do so. If you do not wish to do so, delete this | |
| exception statement from your version. */ | |
| package gnu.java.net.protocol.http; | |
| import gnu.classpath.SystemProperties; | |
| import gnu.java.lang.CPStringBuilder; | |
| import gnu.java.net.EmptyX509TrustManager; | |
| import java.io.BufferedInputStream; | |
| import java.io.BufferedOutputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.net.InetSocketAddress; | |
| import java.net.Socket; | |
| import java.net.SocketException; | |
| import java.security.GeneralSecurityException; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.Iterator; | |
| import java.util.LinkedList; | |
| import java.util.List; | |
| import java.util.ListIterator; | |
| import java.util.Map; | |
| import javax.net.ssl.HandshakeCompletedListener; | |
| import javax.net.ssl.SSLContext; | |
| import javax.net.ssl.SSLSocket; | |
| import javax.net.ssl.SSLSocketFactory; | |
| import javax.net.ssl.TrustManager; | |
| /** | |
| * A connection to an HTTP server. | |
| * | |
| * @author Chris Burdess (dog@gnu.org) | |
| */ | |
| public class HTTPConnection | |
| { | |
| /** | |
| * The default HTTP port. | |
| */ | |
| public static final int HTTP_PORT = 80; | |
| /** | |
| * The default HTTPS port. | |
| */ | |
| public static final int HTTPS_PORT = 443; | |
| private static final String userAgent = SystemProperties.getProperty("http.agent"); | |
| /** | |
| * The host name of the server to connect to. | |
| */ | |
| protected final String hostname; | |
| /** | |
| * The port to connect to. | |
| */ | |
| protected final int port; | |
| /** | |
| * Whether the connection should use transport level security (HTTPS). | |
| */ | |
| protected final boolean secure; | |
| /** | |
| * The connection timeout for connecting the underlying socket. | |
| */ | |
| protected final int connectionTimeout; | |
| /** | |
| * The read timeout for reads on the underlying socket. | |
| */ | |
| protected final int timeout; | |
| /** | |
| * The host name of the proxy to connect to. | |
| */ | |
| protected String proxyHostname; | |
| /** | |
| * The port on the proxy to connect to. | |
| */ | |
| protected int proxyPort; | |
| /** | |
| * The major version of HTTP supported by this client. | |
| */ | |
| protected int majorVersion; | |
| /** | |
| * The minor version of HTTP supported by this client. | |
| */ | |
| protected int minorVersion; | |
| private final List<HandshakeCompletedListener> handshakeCompletedListeners; | |
| /** | |
| * The socket this connection communicates on. | |
| */ | |
| protected Socket socket; | |
| /** | |
| * The SSL socket factory to use. | |
| */ | |
| private SSLSocketFactory sslSocketFactory; | |
| /** | |
| * The socket input stream. | |
| */ | |
| protected InputStream in; | |
| /** | |
| * The socket output stream. | |
| */ | |
| protected OutputStream out; | |
| /** | |
| * Nonce values seen by this connection. | |
| */ | |
| private Map<String, Integer> nonceCounts; | |
| /** | |
| * The cookie manager for this connection. | |
| */ | |
| protected CookieManager cookieManager; | |
| /** | |
| * The pool that this connection is a member of (if any). | |
| */ | |
| private Pool pool; | |
| /** | |
| * Creates a new HTTP connection. | |
| * @param hostname the name of the host to connect to | |
| */ | |
| public HTTPConnection(String hostname) | |
| { | |
| this(hostname, HTTP_PORT, false, 0, 0); | |
| } | |
| /** | |
| * Creates a new HTTP or HTTPS connection. | |
| * @param hostname the name of the host to connect to | |
| * @param secure whether to use a secure connection | |
| */ | |
| public HTTPConnection(String hostname, boolean secure) | |
| { | |
| this(hostname, secure ? HTTPS_PORT : HTTP_PORT, secure, 0, 0); | |
| } | |
| /** | |
| * Creates a new HTTP or HTTPS connection on the specified port. | |
| * @param hostname the name of the host to connect to | |
| * @param secure whether to use a secure connection | |
| * @param connectionTimeout the connection timeout | |
| * @param timeout the socket read timeout | |
| */ | |
| public HTTPConnection(String hostname, boolean secure, | |
| int connectionTimeout, int timeout) | |
| { | |
| this(hostname, secure ? HTTPS_PORT : HTTP_PORT, secure, | |
| connectionTimeout, timeout); | |
| } | |
| /** | |
| * Creates a new HTTP connection on the specified port. | |
| * @param hostname the name of the host to connect to | |
| * @param port the port on the host to connect to | |
| */ | |
| public HTTPConnection(String hostname, int port) | |
| { | |
| this(hostname, port, false, 0, 0); | |
| } | |
| /** | |
| * Creates a new HTTP or HTTPS connection on the specified port. | |
| * @param hostname the name of the host to connect to | |
| * @param port the port on the host to connect to | |
| * @param secure whether to use a secure connection | |
| */ | |
| public HTTPConnection(String hostname, int port, boolean secure) | |
| { | |
| this(hostname, port, secure, 0, 0); | |
| } | |
| /** | |
| * Creates a new HTTP or HTTPS connection on the specified port. | |
| * @param hostname the name of the host to connect to | |
| * @param port the port on the host to connect to | |
| * @param secure whether to use a secure connection | |
| * @param connectionTimeout the connection timeout | |
| * @param timeout the socket read timeout | |
| * | |
| * @throws IllegalArgumentException if either connectionTimeout or | |
| * timeout less than zero. | |
| */ | |
| public HTTPConnection(String hostname, int port, boolean secure, | |
| int connectionTimeout, int timeout) | |
| { | |
| if (connectionTimeout < 0 || timeout < 0) | |
| throw new IllegalArgumentException(); | |
| this.hostname = hostname; | |
| this.port = port; | |
| this.secure = secure; | |
| this.connectionTimeout = connectionTimeout; | |
| this.timeout = timeout; | |
| majorVersion = minorVersion = 1; | |
| handshakeCompletedListeners | |
| = new ArrayList<HandshakeCompletedListener>(2); | |
| } | |
| /** | |
| * Returns the name of the host to connect to. | |
| */ | |
| public String getHostName() | |
| { | |
| return hostname; | |
| } | |
| /** | |
| * Returns the port on the host to connect to. | |
| */ | |
| public int getPort() | |
| { | |
| return port; | |
| } | |
| /** | |
| * Indicates whether to use a secure connection or not. | |
| */ | |
| public boolean isSecure() | |
| { | |
| return secure; | |
| } | |
| /** | |
| * Returns the HTTP version string supported by this connection. | |
| * @see #majorVersion | |
| * @see #minorVersion | |
| */ | |
| public String getVersion() | |
| { | |
| return "HTTP/" + majorVersion + '.' + minorVersion; | |
| } | |
| /** | |
| * Sets the HTTP version supported by this connection. | |
| * @param majorVersion the major version | |
| * @param minorVersion the minor version | |
| */ | |
| public void setVersion(int majorVersion, int minorVersion) | |
| { | |
| if (majorVersion != 1) | |
| { | |
| throw new IllegalArgumentException("major version not supported: " + | |
| majorVersion); | |
| } | |
| if (minorVersion < 0 || minorVersion > 1) | |
| { | |
| throw new IllegalArgumentException("minor version not supported: " + | |
| minorVersion); | |
| } | |
| this.majorVersion = majorVersion; | |
| this.minorVersion = minorVersion; | |
| } | |
| /** | |
| * Directs this connection to use the specified proxy. | |
| * @param hostname the proxy host name | |
| * @param port the port on the proxy to connect to | |
| */ | |
| public void setProxy(String hostname, int port) | |
| { | |
| proxyHostname = hostname; | |
| proxyPort = port; | |
| } | |
| /** | |
| * Indicates whether this connection is using an HTTP proxy. | |
| */ | |
| public boolean isUsingProxy() | |
| { | |
| return (proxyHostname != null && proxyPort > 0); | |
| } | |
| /** | |
| * Sets the cookie manager to use for this connection. | |
| * @param cookieManager the cookie manager | |
| */ | |
| public void setCookieManager(CookieManager cookieManager) | |
| { | |
| this.cookieManager = cookieManager; | |
| } | |
| /** | |
| * Returns the cookie manager in use for this connection. | |
| */ | |
| public CookieManager getCookieManager() | |
| { | |
| return cookieManager; | |
| } | |
| /** | |
| * Manages a pool of HTTPConections. The pool will have a maximum | |
| * size determined by the value of the maxConn parameter passed to | |
| * the {@link #get} method. This value inevitably comes from the | |
| * http.maxConnections system property. If the | |
| * classpath.net.http.keepAliveTTL system property is set, that will | |
| * be the maximum time (in seconds) that an idle connection will be | |
| * maintained. | |
| */ | |
| static class Pool | |
| { | |
| /** | |
| * Singleton instance of the pool. | |
| */ | |
| static Pool instance = new Pool(); | |
| /** | |
| * The pool | |
| */ | |
| final LinkedList<HTTPConnection> connectionPool | |
| = new LinkedList<HTTPConnection>(); | |
| /** | |
| * Maximum size of the pool. | |
| */ | |
| int maxConnections; | |
| /** | |
| * If greater than zero, the maximum time a connection will remain | |
| * int the pool. | |
| */ | |
| int connectionTTL; | |
| /** | |
| * A thread that removes connections older than connectionTTL. | |
| */ | |
| class Reaper | |
| implements Runnable | |
| { | |
| public void run() | |
| { | |
| synchronized (Pool.this) | |
| { | |
| try | |
| { | |
| do | |
| { | |
| while (connectionPool.size() > 0) | |
| { | |
| long currentTime = System.currentTimeMillis(); | |
| HTTPConnection c = | |
| (HTTPConnection)connectionPool.getFirst(); | |
| long waitTime = c.timeLastUsed | |
| + connectionTTL - currentTime; | |
| if (waitTime <= 0) | |
| removeOldest(); | |
| else | |
| try | |
| { | |
| Pool.this.wait(waitTime); | |
| } | |
| catch (InterruptedException _) | |
| { | |
| // Ignore the interrupt. | |
| } | |
| } | |
| // After the pool is empty, wait TTL to see if it | |
| // is used again. This is because in the | |
| // situation where a single thread is making HTTP | |
| // requests to the same server it can remove the | |
| // connection from the pool before the Reaper has | |
| // a chance to start. This would cause the Reaper | |
| // to exit if it were not for this extra delay. | |
| // The result would be starting a Reaper thread | |
| // for each HTTP request. With the delay we get | |
| // at most one Reaper created each TTL. | |
| try | |
| { | |
| Pool.this.wait(connectionTTL); | |
| } | |
| catch (InterruptedException _) | |
| { | |
| // Ignore the interrupt. | |
| } | |
| } | |
| while (connectionPool.size() > 0); | |
| } | |
| finally | |
| { | |
| reaper = null; | |
| } | |
| } | |
| } | |
| } | |
| Reaper reaper; | |
| /** | |
| * Private constructor to ensure singleton. | |
| */ | |
| private Pool() | |
| { | |
| } | |
| /** | |
| * Tests for a matching connection. | |
| * | |
| * @param c connection to match. | |
| * @param h the host name. | |
| * @param p the port. | |
| * @param sec true if using https. | |
| * | |
| * @return true if c matches h, p, and sec. | |
| */ | |
| private static boolean matches(HTTPConnection c, | |
| String h, int p, boolean sec) | |
| { | |
| return h.equals(c.hostname) && (p == c.port) && (sec == c.secure); | |
| } | |
| /** | |
| * Get a pooled HTTPConnection. If there is an existing idle | |
| * connection to the requested server it is returned. Otherwise a | |
| * new connection is created. | |
| * | |
| * @param host the name of the host to connect to | |
| * @param port the port on the host to connect to | |
| * @param secure whether to use a secure connection | |
| * | |
| * @return the HTTPConnection. | |
| */ | |
| synchronized HTTPConnection get(String host, | |
| int port, | |
| boolean secure, | |
| int connectionTimeout, int timeout) | |
| { | |
| String ttl = | |
| SystemProperties.getProperty("classpath.net.http.keepAliveTTL"); | |
| connectionTTL = 10000; | |
| if (ttl != null && ttl.length() > 0) | |
| try | |
| { | |
| int v = 1000 * Integer.parseInt(ttl); | |
| if (v >= 0) | |
| connectionTTL = v; | |
| } | |
| catch (NumberFormatException _) | |
| { | |
| // Ignore. | |
| } | |
| String mc = SystemProperties.getProperty("http.maxConnections"); | |
| maxConnections = 5; | |
| if (mc != null && mc.length() > 0) | |
| try | |
| { | |
| int v = Integer.parseInt(mc); | |
| if (v > 0) | |
| maxConnections = v; | |
| } | |
| catch (NumberFormatException _) | |
| { | |
| // Ignore. | |
| } | |
| HTTPConnection c = null; | |
| ListIterator it = connectionPool.listIterator(0); | |
| while (it.hasNext()) | |
| { | |
| HTTPConnection cc = (HTTPConnection)it.next(); | |
| if (matches(cc, host, port, secure)) | |
| { | |
| c = cc; | |
| it.remove(); | |
| // Update the timeout. | |
| if (c.socket != null) | |
| try | |
| { | |
| c.socket.setSoTimeout(timeout); | |
| } | |
| catch (SocketException _) | |
| { | |
| // Ignore. | |
| } | |
| break; | |
| } | |
| } | |
| if (c == null) | |
| { | |
| c = new HTTPConnection(host, port, secure, | |
| connectionTimeout, timeout); | |
| c.setPool(this); | |
| } | |
| return c; | |
| } | |
| /** | |
| * Put an idle HTTPConnection back into the pool. If this causes | |
| * the pool to be come too large, the oldest connection is removed | |
| * and closed. | |
| * | |
| */ | |
| synchronized void put(HTTPConnection c) | |
| { | |
| c.timeLastUsed = System.currentTimeMillis(); | |
| connectionPool.addLast(c); | |
| // maxConnections must always be >= 1 | |
| while (connectionPool.size() >= maxConnections) | |
| removeOldest(); | |
| if (connectionTTL > 0 && null == reaper) { | |
| // If there is a connectionTTL, then the reaper has removed | |
| // any stale connections, so we don't have to check for stale | |
| // now. We do have to start a reaper though, as there is not | |
| // one running now. | |
| reaper = new Reaper(); | |
| Thread t = new Thread(reaper, "HTTPConnection.Reaper"); | |
| t.setDaemon(true); | |
| t.start(); | |
| } | |
| } | |
| /** | |
| * Remove the oldest connection from the pool and close it. | |
| */ | |
| void removeOldest() | |
| { | |
| HTTPConnection cx = (HTTPConnection)connectionPool.removeFirst(); | |
| try | |
| { | |
| cx.closeConnection(); | |
| } | |
| catch (IOException ioe) | |
| { | |
| // Ignore it. We are just cleaning up. | |
| } | |
| } | |
| } | |
| /** | |
| * The number of times this HTTPConnection has be used via keep-alive. | |
| */ | |
| int useCount; | |
| /** | |
| * If this HTTPConnection is in the pool, the time it was put there. | |
| */ | |
| long timeLastUsed; | |
| /** | |
| * Set the connection pool that this HTTPConnection is a member of. | |
| * If left unset or set to null, it will not be a member of any pool | |
| * and will not be a candidate for reuse. | |
| * | |
| * @param p the pool. | |
| */ | |
| void setPool(Pool p) | |
| { | |
| pool = p; | |
| } | |
| /** | |
| * Signal that this HTTPConnection is no longer needed and can be | |
| * returned to the connection pool. | |
| * | |
| */ | |
| void release() | |
| { | |
| if (pool != null) | |
| { | |
| useCount++; | |
| pool.put(this); | |
| } | |
| else | |
| { | |
| // If there is no pool, just close. | |
| try | |
| { | |
| closeConnection(); | |
| } | |
| catch (IOException ioe) | |
| { | |
| // Ignore it. We are just cleaning up. | |
| } | |
| } | |
| } | |
| /** | |
| * Creates a new request using this connection. | |
| * @param method the HTTP method to invoke | |
| * @param path the URI-escaped RFC2396 <code>abs_path</code> with | |
| * optional query part | |
| */ | |
| public Request newRequest(String method, String path) | |
| { | |
| if (method == null || method.length() == 0) | |
| { | |
| throw new IllegalArgumentException("method must have non-zero length"); | |
| } | |
| if (path == null || path.length() == 0) | |
| { | |
| path = "/"; | |
| } | |
| Request ret = new Request(this, method, path); | |
| if ((secure && port != HTTPS_PORT) || | |
| (!secure && port != HTTP_PORT)) | |
| { | |
| ret.setHeader("Host", hostname + ":" + port); | |
| } | |
| else | |
| { | |
| ret.setHeader("Host", hostname); | |
| } | |
| ret.setHeader("User-Agent", userAgent); | |
| ret.setHeader("Connection", "keep-alive"); | |
| ret.setHeader("Accept-Encoding", | |
| "chunked;q=1.0, gzip;q=0.9, deflate;q=0.8, " + | |
| "identity;q=0.6, *;q=0"); | |
| if (cookieManager != null) | |
| { | |
| Cookie[] cookies = cookieManager.getCookies(hostname, secure, path); | |
| if (cookies != null && cookies.length > 0) | |
| { | |
| CPStringBuilder buf = new CPStringBuilder(); | |
| buf.append("$Version=1"); | |
| for (int i = 0; i < cookies.length; i++) | |
| { | |
| buf.append(','); | |
| buf.append(' '); | |
| buf.append(cookies[i].toString()); | |
| } | |
| ret.setHeader("Cookie", buf.toString()); | |
| } | |
| } | |
| return ret; | |
| } | |
| /** | |
| * Closes this connection. | |
| */ | |
| public void close() | |
| throws IOException | |
| { | |
| closeConnection(); | |
| } | |
| /** | |
| * Retrieves the socket associated with this connection. | |
| * This creates the socket if necessary. | |
| */ | |
| protected synchronized Socket getSocket() | |
| throws IOException | |
| { | |
| if (socket == null) | |
| { | |
| String connectHostname = hostname; | |
| int connectPort = port; | |
| if (isUsingProxy()) | |
| { | |
| connectHostname = proxyHostname; | |
| connectPort = proxyPort; | |
| } | |
| socket = new Socket(); | |
| InetSocketAddress address = | |
| new InetSocketAddress(connectHostname, connectPort); | |
| if (connectionTimeout > 0) | |
| { | |
| socket.connect(address, connectionTimeout); | |
| } | |
| else | |
| { | |
| socket.connect(address); | |
| } | |
| if (timeout > 0) | |
| { | |
| socket.setSoTimeout(timeout); | |
| } | |
| if (secure) | |
| { | |
| try | |
| { | |
| SSLSocketFactory factory = getSSLSocketFactory(); | |
| SSLSocket ss = | |
| (SSLSocket) factory.createSocket(socket, connectHostname, | |
| connectPort, true); | |
| String[] protocols = { "TLSv1", "SSLv3" }; | |
| ss.setEnabledProtocols(protocols); | |
| ss.setUseClientMode(true); | |
| synchronized (handshakeCompletedListeners) | |
| { | |
| if (!handshakeCompletedListeners.isEmpty()) | |
| { | |
| for (Iterator i = | |
| handshakeCompletedListeners.iterator(); | |
| i.hasNext(); ) | |
| { | |
| HandshakeCompletedListener l = | |
| (HandshakeCompletedListener) i.next(); | |
| ss.addHandshakeCompletedListener(l); | |
| } | |
| } | |
| } | |
| ss.startHandshake(); | |
| socket = ss; | |
| } | |
| catch (GeneralSecurityException e) | |
| { | |
| throw new IOException(e.getMessage()); | |
| } | |
| } | |
| in = socket.getInputStream(); | |
| in = new BufferedInputStream(in); | |
| out = socket.getOutputStream(); | |
| out = new BufferedOutputStream(out); | |
| } | |
| return socket; | |
| } | |
| SSLSocketFactory getSSLSocketFactory() | |
| throws GeneralSecurityException | |
| { | |
| if (sslSocketFactory == null) | |
| { | |
| TrustManager tm = new EmptyX509TrustManager(); | |
| SSLContext context = SSLContext.getInstance("SSL"); | |
| TrustManager[] trust = new TrustManager[] { tm }; | |
| context.init(null, trust, null); | |
| sslSocketFactory = context.getSocketFactory(); | |
| } | |
| return sslSocketFactory; | |
| } | |
| void setSSLSocketFactory(SSLSocketFactory factory) | |
| { | |
| sslSocketFactory = factory; | |
| } | |
| protected synchronized InputStream getInputStream() | |
| throws IOException | |
| { | |
| if (socket == null) | |
| { | |
| getSocket(); | |
| } | |
| return in; | |
| } | |
| protected synchronized OutputStream getOutputStream() | |
| throws IOException | |
| { | |
| if (socket == null) | |
| { | |
| getSocket(); | |
| } | |
| return out; | |
| } | |
| /** | |
| * Closes the underlying socket, if any. | |
| */ | |
| protected synchronized void closeConnection() | |
| throws IOException | |
| { | |
| if (socket != null) | |
| { | |
| try | |
| { | |
| socket.close(); | |
| } | |
| finally | |
| { | |
| socket = null; | |
| } | |
| } | |
| } | |
| /** | |
| * Returns a URI representing the connection. | |
| * This does not include any request path component. | |
| */ | |
| protected String getURI() | |
| { | |
| CPStringBuilder buf = new CPStringBuilder(); | |
| buf.append(secure ? "https://" : "http://"); | |
| buf.append(hostname); | |
| if (secure) | |
| { | |
| if (port != HTTPConnection.HTTPS_PORT) | |
| { | |
| buf.append(':'); | |
| buf.append(port); | |
| } | |
| } | |
| else | |
| { | |
| if (port != HTTPConnection.HTTP_PORT) | |
| { | |
| buf.append(':'); | |
| buf.append(port); | |
| } | |
| } | |
| return buf.toString(); | |
| } | |
| /** | |
| * Get the number of times the specified nonce has been seen by this | |
| * connection. | |
| */ | |
| int getNonceCount(String nonce) | |
| { | |
| if (nonceCounts == null) | |
| { | |
| return 0; | |
| } | |
| return nonceCounts.get(nonce).intValue(); | |
| } | |
| /** | |
| * Increment the number of times the specified nonce has been seen. | |
| */ | |
| void incrementNonce(String nonce) | |
| { | |
| int current = getNonceCount(nonce); | |
| if (nonceCounts == null) | |
| { | |
| nonceCounts = new HashMap<String, Integer>(); | |
| } | |
| nonceCounts.put(nonce, new Integer(current + 1)); | |
| } | |
| // -- Events -- | |
| void addHandshakeCompletedListener(HandshakeCompletedListener l) | |
| { | |
| synchronized (handshakeCompletedListeners) | |
| { | |
| handshakeCompletedListeners.add(l); | |
| } | |
| } | |
| void removeHandshakeCompletedListener(HandshakeCompletedListener l) | |
| { | |
| synchronized (handshakeCompletedListeners) | |
| { | |
| handshakeCompletedListeners.remove(l); | |
| } | |
| } | |
| } |