1 /* HTTPConnection.java --
2    Copyright (C) 2004, 2005, 2006  Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 
39 package gnu.java.net.protocol.http;
40 
41 import gnu.classpath.SystemProperties;
42 
43 import gnu.java.lang.CPStringBuilder;
44 import gnu.java.net.EmptyX509TrustManager;
45 
46 import java.io.BufferedInputStream;
47 import java.io.BufferedOutputStream;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.io.OutputStream;
51 import java.net.InetSocketAddress;
52 import java.net.Socket;
53 import java.net.SocketException;
54 import java.security.GeneralSecurityException;
55 import java.util.ArrayList;
56 import java.util.HashMap;
57 import java.util.Iterator;
58 import java.util.LinkedList;
59 import java.util.List;
60 import java.util.ListIterator;
61 import java.util.Map;
62 
63 import javax.net.ssl.HandshakeCompletedListener;
64 import javax.net.ssl.SSLContext;
65 import javax.net.ssl.SSLSocket;
66 import javax.net.ssl.SSLSocketFactory;
67 import javax.net.ssl.TrustManager;
68 
69 /**
70  * A connection to an HTTP server.
71  *
72  * @author Chris Burdess (dog@gnu.org)
73  */
74 public class HTTPConnection
75 {
76 
77   /**
78    * The default HTTP port.
79    */
80   public static final int HTTP_PORT = 80;
81 
82   /**
83    * The default HTTPS port.
84    */
85   public static final int HTTPS_PORT = 443;
86 
87   private static final String userAgent = SystemProperties.getProperty("http.agent");
88 
89   /**
90    * The host name of the server to connect to.
91    */
92   protected final String hostname;
93 
94   /**
95    * The port to connect to.
96    */
97   protected final int port;
98 
99   /**
100    * Whether the connection should use transport level security (HTTPS).
101    */
102   protected final boolean secure;
103 
104   /**
105    * The connection timeout for connecting the underlying socket.
106    */
107   protected final int connectionTimeout;
108 
109   /**
110    * The read timeout for reads on the underlying socket.
111    */
112   protected final int timeout;
113 
114   /**
115    * The host name of the proxy to connect to.
116    */
117   protected String proxyHostname;
118 
119   /**
120    * The port on the proxy to connect to.
121    */
122   protected int proxyPort;
123 
124   /**
125    * The major version of HTTP supported by this client.
126    */
127   protected int majorVersion;
128 
129   /**
130    * The minor version of HTTP supported by this client.
131    */
132   protected int minorVersion;
133 
134   private final List<HandshakeCompletedListener> handshakeCompletedListeners;
135 
136   /**
137    * The socket this connection communicates on.
138    */
139   protected Socket socket;
140 
141   /**
142    * The SSL socket factory to use.
143    */
144   private SSLSocketFactory sslSocketFactory;
145 
146   /**
147    * The socket input stream.
148    */
149   protected InputStream in;
150 
151   /**
152    * The socket output stream.
153    */
154   protected OutputStream out;
155 
156   /**
157    * Nonce values seen by this connection.
158    */
159   private Map<String, Integer> nonceCounts;
160 
161   /**
162    * The cookie manager for this connection.
163    */
164   protected CookieManager cookieManager;
165 
166 
167   /**
168    * The pool that this connection is a member of (if any).
169    */
170   private Pool pool;
171 
172   /**
173    * Creates a new HTTP connection.
174    * @param hostname the name of the host to connect to
175    */
HTTPConnection(String hostname)176   public HTTPConnection(String hostname)
177   {
178     this(hostname, HTTP_PORT, false, 0, 0);
179   }
180 
181   /**
182    * Creates a new HTTP or HTTPS connection.
183    * @param hostname the name of the host to connect to
184    * @param secure whether to use a secure connection
185    */
HTTPConnection(String hostname, boolean secure)186   public HTTPConnection(String hostname, boolean secure)
187   {
188     this(hostname, secure ? HTTPS_PORT : HTTP_PORT, secure, 0, 0);
189   }
190 
191   /**
192    * Creates a new HTTP or HTTPS connection on the specified port.
193    * @param hostname the name of the host to connect to
194    * @param secure whether to use a secure connection
195    * @param connectionTimeout the connection timeout
196    * @param timeout the socket read timeout
197    */
HTTPConnection(String hostname, boolean secure, int connectionTimeout, int timeout)198   public HTTPConnection(String hostname, boolean secure,
199                         int connectionTimeout, int timeout)
200   {
201     this(hostname, secure ? HTTPS_PORT : HTTP_PORT, secure,
202          connectionTimeout, timeout);
203   }
204 
205   /**
206    * Creates a new HTTP connection on the specified port.
207    * @param hostname the name of the host to connect to
208    * @param port the port on the host to connect to
209    */
HTTPConnection(String hostname, int port)210   public HTTPConnection(String hostname, int port)
211   {
212     this(hostname, port, false, 0, 0);
213   }
214 
215   /**
216    * Creates a new HTTP or HTTPS connection on the specified port.
217    * @param hostname the name of the host to connect to
218    * @param port the port on the host to connect to
219    * @param secure whether to use a secure connection
220    */
HTTPConnection(String hostname, int port, boolean secure)221   public HTTPConnection(String hostname, int port, boolean secure)
222   {
223     this(hostname, port, secure, 0, 0);
224   }
225 
226   /**
227    * Creates a new HTTP or HTTPS connection on the specified port.
228    * @param hostname the name of the host to connect to
229    * @param port the port on the host to connect to
230    * @param secure whether to use a secure connection
231    * @param connectionTimeout the connection timeout
232    * @param timeout the socket read timeout
233    *
234    * @throws IllegalArgumentException if either connectionTimeout or
235    * timeout less than zero.
236    */
HTTPConnection(String hostname, int port, boolean secure, int connectionTimeout, int timeout)237   public HTTPConnection(String hostname, int port, boolean secure,
238                         int connectionTimeout, int timeout)
239   {
240     if (connectionTimeout < 0 || timeout < 0)
241       throw new IllegalArgumentException();
242 
243     this.hostname = hostname;
244     this.port = port;
245     this.secure = secure;
246     this.connectionTimeout = connectionTimeout;
247     this.timeout = timeout;
248     majorVersion = minorVersion = 1;
249     handshakeCompletedListeners
250       = new ArrayList<HandshakeCompletedListener>(2);
251   }
252 
253   /**
254    * Returns the name of the host to connect to.
255    */
getHostName()256   public String getHostName()
257   {
258     return hostname;
259   }
260 
261   /**
262    * Returns the port on the host to connect to.
263    */
getPort()264   public int getPort()
265   {
266     return port;
267   }
268 
269   /**
270    * Indicates whether to use a secure connection or not.
271    */
isSecure()272   public boolean isSecure()
273   {
274     return secure;
275   }
276 
277   /**
278    * Returns the HTTP version string supported by this connection.
279    * @see #majorVersion
280    * @see #minorVersion
281    */
getVersion()282   public String getVersion()
283   {
284     return "HTTP/" + majorVersion + '.' + minorVersion;
285   }
286 
287   /**
288    * Sets the HTTP version supported by this connection.
289    * @param majorVersion the major version
290    * @param minorVersion the minor version
291    */
setVersion(int majorVersion, int minorVersion)292   public void setVersion(int majorVersion, int minorVersion)
293   {
294     if (majorVersion != 1)
295       {
296         throw new IllegalArgumentException("major version not supported: " +
297                                            majorVersion);
298       }
299     if (minorVersion < 0 || minorVersion > 1)
300       {
301         throw new IllegalArgumentException("minor version not supported: " +
302                                            minorVersion);
303       }
304     this.majorVersion = majorVersion;
305     this.minorVersion = minorVersion;
306   }
307 
308   /**
309    * Directs this connection to use the specified proxy.
310    * @param hostname the proxy host name
311    * @param port the port on the proxy to connect to
312    */
setProxy(String hostname, int port)313   public void setProxy(String hostname, int port)
314   {
315     proxyHostname = hostname;
316     proxyPort = port;
317   }
318 
319   /**
320    * Indicates whether this connection is using an HTTP proxy.
321    */
isUsingProxy()322   public boolean isUsingProxy()
323   {
324     return (proxyHostname != null && proxyPort > 0);
325   }
326 
327   /**
328    * Sets the cookie manager to use for this connection.
329    * @param cookieManager the cookie manager
330    */
setCookieManager(CookieManager cookieManager)331   public void setCookieManager(CookieManager cookieManager)
332   {
333     this.cookieManager = cookieManager;
334   }
335 
336   /**
337    * Returns the cookie manager in use for this connection.
338    */
getCookieManager()339   public CookieManager getCookieManager()
340   {
341     return cookieManager;
342   }
343 
344   /**
345    * Manages a pool of HTTPConections.  The pool will have a maximum
346    * size determined by the value of the maxConn parameter passed to
347    * the {@link #get} method.  This value inevitably comes from the
348    * http.maxConnections system property.  If the
349    * classpath.net.http.keepAliveTTL system property is set, that will
350    * be the maximum time (in seconds) that an idle connection will be
351    * maintained.
352    */
353   static class Pool
354   {
355     /**
356      * Singleton instance of the pool.
357      */
358     static Pool instance = new Pool();
359 
360     /**
361      * The pool
362      */
363     final LinkedList<HTTPConnection> connectionPool
364       = new LinkedList<HTTPConnection>();
365 
366     /**
367      * Maximum size of the pool.
368      */
369     int maxConnections;
370 
371     /**
372      * If greater than zero, the maximum time a connection will remain
373      * int the pool.
374      */
375     int connectionTTL;
376 
377     /**
378      * A thread that removes connections older than connectionTTL.
379      */
380     class Reaper
381       implements Runnable
382     {
run()383       public void run()
384       {
385         synchronized (Pool.this)
386           {
387             try
388               {
389                 do
390                   {
391                     while (connectionPool.size() > 0)
392                       {
393                         long currentTime = System.currentTimeMillis();
394 
395                         HTTPConnection c =
396                           (HTTPConnection)connectionPool.getFirst();
397 
398                         long waitTime = c.timeLastUsed
399                           + connectionTTL - currentTime;
400 
401                         if (waitTime <= 0)
402                           removeOldest();
403                         else
404                           try
405                             {
406                               Pool.this.wait(waitTime);
407                             }
408                           catch (InterruptedException _)
409                             {
410                               // Ignore the interrupt.
411                             }
412                       }
413                     // After the pool is empty, wait TTL to see if it
414                     // is used again.  This is because in the
415                     // situation where a single thread is making HTTP
416                     // requests to the same server it can remove the
417                     // connection from the pool before the Reaper has
418                     // a chance to start.  This would cause the Reaper
419                     // to exit if it were not for this extra delay.
420                     // The result would be starting a Reaper thread
421                     // for each HTTP request.  With the delay we get
422                     // at most one Reaper created each TTL.
423                     try
424                       {
425                         Pool.this.wait(connectionTTL);
426                       }
427                     catch (InterruptedException _)
428                       {
429                         // Ignore the interrupt.
430                       }
431                   }
432                 while (connectionPool.size() > 0);
433               }
434             finally
435               {
436                 reaper = null;
437               }
438           }
439       }
440     }
441 
442     Reaper reaper;
443 
444     /**
445      * Private constructor to ensure singleton.
446      */
Pool()447     private Pool()
448     {
449     }
450 
451     /**
452      * Tests for a matching connection.
453      *
454      * @param c connection to match.
455      * @param h the host name.
456      * @param p the port.
457      * @param sec true if using https.
458      *
459      * @return true if c matches h, p, and sec.
460      */
matches(HTTPConnection c, String h, int p, boolean sec)461     private static boolean matches(HTTPConnection c,
462                                    String h, int p, boolean sec)
463     {
464       return h.equals(c.hostname) && (p == c.port) && (sec == c.secure);
465     }
466 
467     /**
468      * Get a pooled HTTPConnection.  If there is an existing idle
469      * connection to the requested server it is returned.  Otherwise a
470      * new connection is created.
471      *
472      * @param host the name of the host to connect to
473      * @param port the port on the host to connect to
474      * @param secure whether to use a secure connection
475      *
476      * @return the HTTPConnection.
477      */
get(String host, int port, boolean secure, int connectionTimeout, int timeout)478     synchronized HTTPConnection get(String host,
479                                     int port,
480                                     boolean secure,
481                                     int connectionTimeout, int timeout)
482     {
483       String ttl =
484         SystemProperties.getProperty("classpath.net.http.keepAliveTTL");
485       connectionTTL = 10000;
486       if (ttl != null && ttl.length() > 0)
487         try
488           {
489             int v = 1000 * Integer.parseInt(ttl);
490             if (v >= 0)
491               connectionTTL = v;
492           }
493         catch (NumberFormatException _)
494           {
495             // Ignore.
496           }
497 
498       String mc = SystemProperties.getProperty("http.maxConnections");
499       maxConnections = 5;
500       if (mc != null && mc.length() > 0)
501         try
502           {
503             int v = Integer.parseInt(mc);
504             if (v > 0)
505               maxConnections = v;
506           }
507         catch (NumberFormatException _)
508           {
509             // Ignore.
510           }
511 
512       HTTPConnection c = null;
513 
514       ListIterator it = connectionPool.listIterator(0);
515       while (it.hasNext())
516         {
517           HTTPConnection cc = (HTTPConnection)it.next();
518           if (matches(cc, host, port, secure))
519             {
520               c = cc;
521               it.remove();
522               // Update the timeout.
523               if (c.socket != null)
524                 try
525                   {
526                     c.socket.setSoTimeout(timeout);
527                   }
528                 catch (SocketException _)
529                   {
530                     // Ignore.
531                   }
532               break;
533             }
534         }
535       if (c == null)
536         {
537           c = new HTTPConnection(host, port, secure,
538                                  connectionTimeout, timeout);
539           c.setPool(this);
540         }
541       return c;
542     }
543 
544     /**
545      * Put an idle HTTPConnection back into the pool.  If this causes
546      * the pool to be come too large, the oldest connection is removed
547      * and closed.
548      *
549      */
put(HTTPConnection c)550     synchronized void put(HTTPConnection c)
551     {
552       c.timeLastUsed = System.currentTimeMillis();
553       connectionPool.addLast(c);
554 
555       // maxConnections must always be >= 1
556       while (connectionPool.size() >= maxConnections)
557         removeOldest();
558 
559       if (connectionTTL > 0 && null == reaper) {
560         // If there is a connectionTTL, then the reaper has removed
561         // any stale connections, so we don't have to check for stale
562         // now.  We do have to start a reaper though, as there is not
563         // one running now.
564         reaper = new Reaper();
565         Thread t = new Thread(reaper, "HTTPConnection.Reaper");
566         t.setDaemon(true);
567         t.start();
568       }
569     }
570 
571     /**
572      * Remove the oldest connection from the pool and close it.
573      */
removeOldest()574     void removeOldest()
575     {
576       HTTPConnection cx = (HTTPConnection)connectionPool.removeFirst();
577       try
578         {
579           cx.closeConnection();
580         }
581       catch (IOException ioe)
582         {
583           // Ignore it.  We are just cleaning up.
584         }
585     }
586   }
587 
588   /**
589    * The number of times this HTTPConnection has be used via keep-alive.
590    */
591   int useCount;
592 
593   /**
594    * If this HTTPConnection is in the pool, the time it was put there.
595    */
596   long timeLastUsed;
597 
598   /**
599    * Set the connection pool that this HTTPConnection is a member of.
600    * If left unset or set to null, it will not be a member of any pool
601    * and will not be a candidate for reuse.
602    *
603    * @param p the pool.
604    */
setPool(Pool p)605   void setPool(Pool p)
606   {
607     pool = p;
608   }
609 
610   /**
611    * Signal that this HTTPConnection is no longer needed and can be
612    * returned to the connection pool.
613    *
614    */
release()615   void release()
616   {
617     if (pool != null)
618       {
619         useCount++;
620         pool.put(this);
621 
622       }
623     else
624       {
625         // If there is no pool, just close.
626         try
627           {
628             closeConnection();
629           }
630         catch (IOException ioe)
631           {
632             // Ignore it.  We are just cleaning up.
633           }
634       }
635   }
636 
637   /**
638    * Creates a new request using this connection.
639    * @param method the HTTP method to invoke
640    * @param path the URI-escaped RFC2396 <code>abs_path</code> with
641    * optional query part
642    */
newRequest(String method, String path)643   public Request newRequest(String method, String path)
644   {
645     if (method == null || method.length() == 0)
646       {
647         throw new IllegalArgumentException("method must have non-zero length");
648       }
649     if (path == null || path.length() == 0)
650       {
651         path = "/";
652       }
653     Request ret = new Request(this, method, path);
654     if ((secure && port != HTTPS_PORT) ||
655         (!secure && port != HTTP_PORT))
656       {
657         ret.setHeader("Host", hostname + ":" + port);
658       }
659     else
660       {
661         ret.setHeader("Host", hostname);
662       }
663     ret.setHeader("User-Agent", userAgent);
664     ret.setHeader("Connection", "keep-alive");
665     ret.setHeader("Accept-Encoding",
666                   "chunked;q=1.0, gzip;q=0.9, deflate;q=0.8, " +
667                   "identity;q=0.6, *;q=0");
668     if (cookieManager != null)
669       {
670         Cookie[] cookies = cookieManager.getCookies(hostname, secure, path);
671         if (cookies != null && cookies.length > 0)
672           {
673             CPStringBuilder buf = new CPStringBuilder();
674             buf.append("$Version=1");
675             for (int i = 0; i < cookies.length; i++)
676               {
677                 buf.append(',');
678                 buf.append(' ');
679                 buf.append(cookies[i].toString());
680               }
681             ret.setHeader("Cookie", buf.toString());
682           }
683       }
684     return ret;
685   }
686 
687   /**
688    * Closes this connection.
689    */
close()690   public void close()
691     throws IOException
692   {
693     closeConnection();
694   }
695 
696   /**
697    * Retrieves the socket associated with this connection.
698    * This creates the socket if necessary.
699    */
getSocket()700   protected synchronized Socket getSocket()
701     throws IOException
702   {
703     if (socket == null)
704       {
705         String connectHostname = hostname;
706         int connectPort = port;
707         if (isUsingProxy())
708           {
709             connectHostname = proxyHostname;
710             connectPort = proxyPort;
711           }
712         socket = new Socket();
713         InetSocketAddress address =
714           new InetSocketAddress(connectHostname, connectPort);
715         if (connectionTimeout > 0)
716           {
717             socket.connect(address, connectionTimeout);
718           }
719         else
720           {
721             socket.connect(address);
722           }
723         if (timeout > 0)
724           {
725             socket.setSoTimeout(timeout);
726           }
727         if (secure)
728           {
729             try
730               {
731                 SSLSocketFactory factory = getSSLSocketFactory();
732                 SSLSocket ss =
733                   (SSLSocket) factory.createSocket(socket, connectHostname,
734                                                    connectPort, true);
735                 String[] protocols = { "TLSv1", "SSLv3" };
736                 ss.setEnabledProtocols(protocols);
737                 ss.setUseClientMode(true);
738                 synchronized (handshakeCompletedListeners)
739                   {
740                     if (!handshakeCompletedListeners.isEmpty())
741                       {
742                         for (Iterator i =
743                              handshakeCompletedListeners.iterator();
744                              i.hasNext(); )
745                           {
746                             HandshakeCompletedListener l =
747                               (HandshakeCompletedListener) i.next();
748                             ss.addHandshakeCompletedListener(l);
749                           }
750                       }
751                   }
752                 ss.startHandshake();
753                 socket = ss;
754               }
755             catch (GeneralSecurityException e)
756               {
757                 throw new IOException(e.getMessage());
758               }
759           }
760         in = socket.getInputStream();
761         in = new BufferedInputStream(in);
762         out = socket.getOutputStream();
763         out = new BufferedOutputStream(out);
764       }
765     return socket;
766   }
767 
getSSLSocketFactory()768   SSLSocketFactory getSSLSocketFactory()
769     throws GeneralSecurityException
770   {
771     if (sslSocketFactory == null)
772       {
773         TrustManager tm = new EmptyX509TrustManager();
774         SSLContext context = SSLContext.getInstance("SSL");
775         TrustManager[] trust = new TrustManager[] { tm };
776         context.init(null, trust, null);
777         sslSocketFactory = context.getSocketFactory();
778       }
779     return sslSocketFactory;
780   }
781 
setSSLSocketFactory(SSLSocketFactory factory)782   void setSSLSocketFactory(SSLSocketFactory factory)
783   {
784     sslSocketFactory = factory;
785   }
786 
getInputStream()787   protected synchronized InputStream getInputStream()
788     throws IOException
789   {
790     if (socket == null)
791       {
792         getSocket();
793       }
794     return in;
795   }
796 
getOutputStream()797   protected synchronized OutputStream getOutputStream()
798     throws IOException
799   {
800     if (socket == null)
801       {
802         getSocket();
803       }
804     return out;
805   }
806 
807   /**
808    * Closes the underlying socket, if any.
809    */
closeConnection()810   protected synchronized void closeConnection()
811     throws IOException
812   {
813     if (socket != null)
814       {
815         try
816           {
817             socket.close();
818           }
819         finally
820           {
821             socket = null;
822           }
823       }
824   }
825 
826   /**
827    * Returns a URI representing the connection.
828    * This does not include any request path component.
829    */
getURI()830   protected String getURI()
831   {
832     CPStringBuilder buf = new CPStringBuilder();
833     buf.append(secure ? "https://" : "http://");
834     buf.append(hostname);
835     if (secure)
836       {
837         if (port != HTTPConnection.HTTPS_PORT)
838           {
839             buf.append(':');
840             buf.append(port);
841           }
842       }
843     else
844       {
845         if (port != HTTPConnection.HTTP_PORT)
846           {
847             buf.append(':');
848             buf.append(port);
849           }
850       }
851     return buf.toString();
852   }
853 
854   /**
855    * Get the number of times the specified nonce has been seen by this
856    * connection.
857    */
getNonceCount(String nonce)858   int getNonceCount(String nonce)
859   {
860     if (nonceCounts == null)
861       {
862         return 0;
863       }
864     return nonceCounts.get(nonce).intValue();
865   }
866 
867   /**
868    * Increment the number of times the specified nonce has been seen.
869    */
incrementNonce(String nonce)870   void incrementNonce(String nonce)
871   {
872     int current = getNonceCount(nonce);
873     if (nonceCounts == null)
874       {
875         nonceCounts = new HashMap<String, Integer>();
876       }
877     nonceCounts.put(nonce, new Integer(current + 1));
878   }
879 
880   // -- Events --
881 
addHandshakeCompletedListener(HandshakeCompletedListener l)882   void addHandshakeCompletedListener(HandshakeCompletedListener l)
883   {
884     synchronized (handshakeCompletedListeners)
885       {
886         handshakeCompletedListeners.add(l);
887       }
888   }
removeHandshakeCompletedListener(HandshakeCompletedListener l)889   void removeHandshakeCompletedListener(HandshakeCompletedListener l)
890   {
891     synchronized (handshakeCompletedListeners)
892       {
893         handshakeCompletedListeners.remove(l);
894       }
895   }
896 
897 }
898