| /* 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.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.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 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 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 |
| */ |
| public HTTPConnection(String hostname, int port, boolean secure, |
| int connectionTimeout, int timeout) |
| { |
| this.hostname = hostname; |
| this.port = port; |
| this.secure = secure; |
| this.connectionTimeout = connectionTimeout; |
| this.timeout = timeout; |
| majorVersion = minorVersion = 1; |
| handshakeCompletedListeners = new ArrayList(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 connectionPool = new LinkedList(); |
| |
| /** |
| * 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 = (ttl != null && ttl.length() > 0) ? |
| 1000 * Math.max(1, Integer.parseInt(ttl)) : 10000; |
| |
| String mc = SystemProperties.getProperty("http.maxConnections"); |
| maxConnections = (mc != null && mc.length() > 0) ? |
| Math.max(Integer.parseInt(mc), 1) : 5; |
| if (maxConnections < 1) |
| maxConnections = 1; |
| |
| 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(); |
| 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) |
| { |
| StringBuilder buf = new StringBuilder(); |
| 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() |
| { |
| StringBuilder buf = new StringBuilder(); |
| 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((Integer) 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(); |
| } |
| 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); |
| } |
| } |
| |
| } |
| |