Source for gnu.java.net.protocol.http.HTTPConnection

   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:    */
 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:    */
 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:    */
 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:    */
 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:    */
 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:    */
 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:    */
 256:   public String getHostName()
 257:   {
 258:     return hostname;
 259:   }
 260: 
 261:   /**
 262:    * Returns the port on the host to connect to.
 263:    */
 264:   public int getPort()
 265:   {
 266:     return port;
 267:   }
 268: 
 269:   /**
 270:    * Indicates whether to use a secure connection or not.
 271:    */
 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:    */
 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:    */
 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:    */
 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:    */
 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:    */
 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:    */
 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:     {
 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:      */
 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:      */
 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:      */
 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:      */
 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:      */
 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:    */
 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:    */
 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:    */
 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:    */
 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:    */
 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: 
 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: 
 782:   void setSSLSocketFactory(SSLSocketFactory factory)
 783:   {
 784:     sslSocketFactory = factory;
 785:   }
 786: 
 787:   protected synchronized InputStream getInputStream()
 788:     throws IOException
 789:   {
 790:     if (socket == null)
 791:       {
 792:         getSocket();
 793:       }
 794:     return in;
 795:   }
 796: 
 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:    */
 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:    */
 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:    */
 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:    */
 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: 
 882:   void addHandshakeCompletedListener(HandshakeCompletedListener l)
 883:   {
 884:     synchronized (handshakeCompletedListeners)
 885:       {
 886:         handshakeCompletedListeners.add(l);
 887:       }
 888:   }
 889:   void removeHandshakeCompletedListener(HandshakeCompletedListener l)
 890:   {
 891:     synchronized (handshakeCompletedListeners)
 892:       {
 893:         handshakeCompletedListeners.remove(l);
 894:       }
 895:   }
 896: 
 897: }