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

   1: /* HTTPConnection.java --
   2:    Copyright (C) 2004, 2005  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.Configuration;
  42: import gnu.classpath.SystemProperties;
  43: import gnu.java.net.EmptyX509TrustManager;
  44: 
  45: import java.io.BufferedInputStream;
  46: import java.io.BufferedOutputStream;
  47: import java.io.IOException;
  48: import java.io.InputStream;
  49: import java.io.OutputStream;
  50: import java.net.InetSocketAddress;
  51: import java.net.Socket;
  52: import java.security.GeneralSecurityException;
  53: import java.util.ArrayList;
  54: import java.util.HashMap;
  55: import java.util.Iterator;
  56: import java.util.LinkedHashMap;
  57: import java.util.List;
  58: import java.util.Map;
  59: 
  60: import javax.net.ssl.HandshakeCompletedListener;
  61: import javax.net.ssl.SSLContext;
  62: import javax.net.ssl.SSLSocket;
  63: import javax.net.ssl.SSLSocketFactory;
  64: import javax.net.ssl.TrustManager;
  65: 
  66: /**
  67:  * A connection to an HTTP server.
  68:  *
  69:  * @author Chris Burdess (dog@gnu.org)
  70:  */
  71: public class HTTPConnection
  72: {
  73: 
  74:   /**
  75:    * The default HTTP port.
  76:    */
  77:   public static final int HTTP_PORT = 80;
  78: 
  79:   /**
  80:    * The default HTTPS port.
  81:    */
  82:   public static final int HTTPS_PORT = 443;
  83: 
  84:   private static final String userAgent = SystemProperties.getProperty("http.agent");
  85: 
  86:   /**
  87:    * The host name of the server to connect to.
  88:    */
  89:   protected final String hostname;
  90: 
  91:   /**
  92:    * The port to connect to.
  93:    */
  94:   protected final int port;
  95: 
  96:   /**
  97:    * Whether the connection should use transport level security (HTTPS).
  98:    */
  99:   protected final boolean secure;
 100: 
 101:   /**
 102:    * The connection timeout for connecting the underlying socket.
 103:    */
 104:   protected final int connectionTimeout;
 105: 
 106:   /**
 107:    * The read timeout for reads on the underlying socket.
 108:    */
 109:   protected final int timeout;
 110: 
 111:   /**
 112:    * The host name of the proxy to connect to.
 113:    */
 114:   protected String proxyHostname;
 115: 
 116:   /**
 117:    * The port on the proxy to connect to.
 118:    */
 119:   protected int proxyPort;
 120: 
 121:   /**
 122:    * The major version of HTTP supported by this client.
 123:    */
 124:   protected int majorVersion;
 125: 
 126:   /**
 127:    * The minor version of HTTP supported by this client.
 128:    */
 129:   protected int minorVersion;
 130: 
 131:   private final List handshakeCompletedListeners;
 132: 
 133:   /**
 134:    * The socket this connection communicates on.
 135:    */
 136:   protected Socket socket;
 137: 
 138:   /**
 139:    * The SSL socket factory to use.
 140:    */
 141:   private SSLSocketFactory sslSocketFactory;
 142: 
 143:   /**
 144:    * The socket input stream.
 145:    */
 146:   protected InputStream in;
 147: 
 148:   /**
 149:    * The socket output stream.
 150:    */
 151:   protected OutputStream out;
 152: 
 153:   /**
 154:    * Nonce values seen by this connection.
 155:    */
 156:   private Map nonceCounts;
 157: 
 158:   /**
 159:    * The cookie manager for this connection.
 160:    */
 161:   protected CookieManager cookieManager;
 162: 
 163: 
 164:   /**
 165:    * The pool that this connection is a member of (if any).
 166:    */
 167:   private LinkedHashMap pool;
 168: 
 169:   /**
 170:    * Creates a new HTTP connection.
 171:    * @param hostname the name of the host to connect to
 172:    */
 173:   public HTTPConnection(String hostname)
 174:   {
 175:     this(hostname, HTTP_PORT, false, 0, 0);
 176:   }
 177: 
 178:   /**
 179:    * Creates a new HTTP or HTTPS connection.
 180:    * @param hostname the name of the host to connect to
 181:    * @param secure whether to use a secure connection
 182:    */
 183:   public HTTPConnection(String hostname, boolean secure)
 184:   {
 185:     this(hostname, secure ? HTTPS_PORT : HTTP_PORT, secure, 0, 0);
 186:   }
 187: 
 188:   /**
 189:    * Creates a new HTTP or HTTPS connection on the specified port.
 190:    * @param hostname the name of the host to connect to
 191:    * @param secure whether to use a secure connection
 192:    * @param connectionTimeout the connection timeout
 193:    * @param timeout the socket read timeout
 194:    */
 195:   public HTTPConnection(String hostname, boolean secure,
 196:                         int connectionTimeout, int timeout)
 197:   {
 198:     this(hostname, secure ? HTTPS_PORT : HTTP_PORT, secure,
 199:          connectionTimeout, timeout);
 200:   }
 201:   
 202:   /**
 203:    * Creates a new HTTP connection on the specified port.
 204:    * @param hostname the name of the host to connect to
 205:    * @param port the port on the host to connect to
 206:    */
 207:   public HTTPConnection(String hostname, int port)
 208:   {
 209:     this(hostname, port, false, 0, 0);
 210:   }
 211: 
 212:   /**
 213:    * Creates a new HTTP or HTTPS connection on the specified port.
 214:    * @param hostname the name of the host to connect to
 215:    * @param port the port on the host to connect to
 216:    * @param secure whether to use a secure connection
 217:    */
 218:   public HTTPConnection(String hostname, int port, boolean secure)
 219:   {
 220:     this(hostname, port, secure, 0, 0);
 221:   }
 222:   
 223:   /**
 224:    * Creates a new HTTP or HTTPS connection on the specified port.
 225:    * @param hostname the name of the host to connect to
 226:    * @param port the port on the host to connect to
 227:    * @param secure whether to use a secure connection
 228:    * @param connectionTimeout the connection timeout
 229:    * @param timeout the socket read timeout
 230:    */
 231:   public HTTPConnection(String hostname, int port, boolean secure,
 232:                         int connectionTimeout, int timeout)
 233:   {
 234:     this.hostname = hostname;
 235:     this.port = port;
 236:     this.secure = secure;
 237:     this.connectionTimeout = connectionTimeout;
 238:     this.timeout = timeout;
 239:     majorVersion = minorVersion = 1;
 240:     handshakeCompletedListeners = new ArrayList(2);
 241:   }
 242: 
 243:   /**
 244:    * Returns the name of the host to connect to.
 245:    */
 246:   public String getHostName()
 247:   {
 248:     return hostname;
 249:   }
 250: 
 251:   /**
 252:    * Returns the port on the host to connect to.
 253:    */
 254:   public int getPort()
 255:   {
 256:     return port;
 257:   }
 258: 
 259:   /**
 260:    * Indicates whether to use a secure connection or not.
 261:    */
 262:   public boolean isSecure()
 263:   {
 264:     return secure;
 265:   }
 266: 
 267:   /**
 268:    * Returns the HTTP version string supported by this connection.
 269:    * @see #version
 270:    */
 271:   public String getVersion()
 272:   {
 273:     return "HTTP/" + majorVersion + '.' + minorVersion;
 274:   }
 275: 
 276:   /**
 277:    * Sets the HTTP version supported by this connection.
 278:    * @param majorVersion the major version
 279:    * @param minorVersion the minor version
 280:    */
 281:   public void setVersion(int majorVersion, int minorVersion)
 282:   {
 283:     if (majorVersion != 1)
 284:       {
 285:         throw new IllegalArgumentException("major version not supported: " +
 286:                                            majorVersion);
 287:       }
 288:     if (minorVersion < 0 || minorVersion > 1)
 289:       {
 290:         throw new IllegalArgumentException("minor version not supported: " +
 291:                                            minorVersion);
 292:       }
 293:     this.majorVersion = majorVersion;
 294:     this.minorVersion = minorVersion;
 295:   }
 296: 
 297:   /**
 298:    * Directs this connection to use the specified proxy.
 299:    * @param hostname the proxy host name
 300:    * @param port the port on the proxy to connect to
 301:    */
 302:   public void setProxy(String hostname, int port)
 303:   {
 304:     proxyHostname = hostname;
 305:     proxyPort = port;
 306:   }
 307: 
 308:   /**
 309:    * Indicates whether this connection is using an HTTP proxy.
 310:    */
 311:   public boolean isUsingProxy()
 312:   {
 313:     return (proxyHostname != null && proxyPort > 0);
 314:   }
 315: 
 316:   /**
 317:    * Sets the cookie manager to use for this connection.
 318:    * @param cookieManager the cookie manager
 319:    */
 320:   public void setCookieManager(CookieManager cookieManager)
 321:   {
 322:     this.cookieManager = cookieManager;
 323:   }
 324: 
 325:   /**
 326:    * Returns the cookie manager in use for this connection.
 327:    */
 328:   public CookieManager getCookieManager()
 329:   {
 330:     return cookieManager;
 331:   }
 332: 
 333:   /**
 334:    * The number of times this HTTPConnection has be used via keep-alive.
 335:    */
 336:   int useCount;
 337: 
 338:   /**
 339:    * Generates a key for connections in the connection pool.
 340:    *
 341:    * @param h the host name.
 342:    * @param p the port.
 343:    * @param sec true if using https.
 344:    *
 345:    * @return the key.
 346:    */
 347:   static Object getPoolKey(String h, int p, boolean sec)
 348:   {
 349:     StringBuilder buf = new StringBuilder(sec ? "https://" : "http://");
 350:     buf.append(h);
 351:     buf.append(':');
 352:     buf.append(p);
 353:     return buf.toString();
 354:   }
 355: 
 356:   /**
 357:    * Set the connection pool that this HTTPConnection is a member of.
 358:    * If left unset or set to null, it will not be a member of any pool
 359:    * and will not be a candidate for reuse.
 360:    *
 361:    * @param p the pool.
 362:    */
 363:   void setPool(LinkedHashMap p)
 364:   {
 365:     pool = p;
 366:   }
 367: 
 368:   /**
 369:    * Signal that this HTTPConnection is no longer needed and can be
 370:    * returned to the connection pool.
 371:    *
 372:    */
 373:   void release()
 374:   {
 375:     if (pool != null)
 376:       {
 377:         synchronized (pool)
 378:           {
 379:             useCount++;
 380:             Object key = HTTPConnection.getPoolKey(hostname, port, secure);
 381:             pool.put(key, this);
 382:             while (pool.size() >= HTTPURLConnection.maxConnections)
 383:               {
 384:                 // maxConnections must always be >= 1
 385:                 Object lru = pool.keySet().iterator().next();
 386:                 HTTPConnection c = (HTTPConnection)pool.remove(lru);
 387:                 try
 388:                   {
 389:                     c.closeConnection();
 390:                   }
 391:                 catch (IOException ioe)
 392:                   {
 393:                       // Ignore it.  We are just cleaning up.
 394:                   }
 395:               }
 396:           }
 397:       }
 398:   }
 399: 
 400:   /**
 401:    * Creates a new request using this connection.
 402:    * @param method the HTTP method to invoke
 403:    * @param path the URI-escaped RFC2396 <code>abs_path</code> with
 404:    * optional query part
 405:    */
 406:   public Request newRequest(String method, String path)
 407:   {
 408:     if (method == null || method.length() == 0)
 409:       {
 410:         throw new IllegalArgumentException("method must have non-zero length");
 411:       }
 412:     if (path == null || path.length() == 0)
 413:       {
 414:         path = "/";
 415:       }
 416:     Request ret = new Request(this, method, path);
 417:     if ((secure && port != HTTPS_PORT) ||
 418:         (!secure && port != HTTP_PORT))
 419:       {
 420:         ret.setHeader("Host", hostname + ":" + port);
 421:       }
 422:     else
 423:       {
 424:         ret.setHeader("Host", hostname);
 425:       }
 426:     ret.setHeader("User-Agent", userAgent);
 427:     ret.setHeader("Connection", "keep-alive");
 428:     ret.setHeader("Accept-Encoding",
 429:                   "chunked;q=1.0, gzip;q=0.9, deflate;q=0.8, " +
 430:                   "identity;q=0.6, *;q=0");
 431:     if (cookieManager != null)
 432:       {
 433:         Cookie[] cookies = cookieManager.getCookies(hostname, secure, path);
 434:         if (cookies != null && cookies.length > 0)
 435:           {
 436:             StringBuilder buf = new StringBuilder();
 437:             buf.append("$Version=1");
 438:             for (int i = 0; i < cookies.length; i++)
 439:               {
 440:                 buf.append(',');
 441:                 buf.append(' ');
 442:                 buf.append(cookies[i].toString());
 443:               }
 444:             ret.setHeader("Cookie", buf.toString());
 445:           }
 446:       }
 447:     return ret;
 448:   }
 449: 
 450:   /**
 451:    * Closes this connection.
 452:    */
 453:   public void close()
 454:     throws IOException
 455:   {
 456:     closeConnection();
 457:   }
 458: 
 459:   /**
 460:    * Retrieves the socket associated with this connection.
 461:    * This creates the socket if necessary.
 462:    */
 463:   protected synchronized Socket getSocket()
 464:     throws IOException
 465:   {
 466:     if (socket == null)
 467:       {
 468:         String connectHostname = hostname;
 469:         int connectPort = port;
 470:         if (isUsingProxy())
 471:           {
 472:             connectHostname = proxyHostname;
 473:             connectPort = proxyPort;
 474:           }
 475:         socket = new Socket();
 476:         InetSocketAddress address =
 477:           new InetSocketAddress(connectHostname, connectPort);
 478:         if (connectionTimeout > 0)
 479:           {
 480:             socket.connect(address, connectionTimeout);
 481:           }
 482:         else
 483:           {
 484:             socket.connect(address);
 485:           }
 486:         if (timeout > 0)
 487:           {
 488:             socket.setSoTimeout(timeout);
 489:           }
 490:         if (secure)
 491:           {
 492:             try
 493:               {
 494:                 SSLSocketFactory factory = getSSLSocketFactory();
 495:                 SSLSocket ss =
 496:                   (SSLSocket) factory.createSocket(socket, connectHostname,
 497:                                                    connectPort, true);
 498:                 String[] protocols = { "TLSv1", "SSLv3" };
 499:                 ss.setEnabledProtocols(protocols);
 500:                 ss.setUseClientMode(true);
 501:                 synchronized (handshakeCompletedListeners)
 502:                   {
 503:                     if (!handshakeCompletedListeners.isEmpty())
 504:                       {
 505:                         for (Iterator i =
 506:                              handshakeCompletedListeners.iterator();
 507:                              i.hasNext(); )
 508:                           {
 509:                             HandshakeCompletedListener l =
 510:                               (HandshakeCompletedListener) i.next();
 511:                             ss.addHandshakeCompletedListener(l);
 512:                           }
 513:                       }
 514:                   }
 515:                 ss.startHandshake();
 516:                 socket = ss;
 517:               }
 518:             catch (GeneralSecurityException e)
 519:               {
 520:                 throw new IOException(e.getMessage());
 521:               }
 522:           }
 523:         in = socket.getInputStream();
 524:         in = new BufferedInputStream(in);
 525:         out = socket.getOutputStream();
 526:         out = new BufferedOutputStream(out);
 527:       }
 528:     return socket;
 529:   }
 530: 
 531:   SSLSocketFactory getSSLSocketFactory()
 532:     throws GeneralSecurityException
 533:   {
 534:     if (sslSocketFactory == null)
 535:       {
 536:         TrustManager tm = new EmptyX509TrustManager();
 537:         SSLContext context = SSLContext.getInstance("SSL");
 538:         TrustManager[] trust = new TrustManager[] { tm };
 539:         context.init(null, trust, null);
 540:         sslSocketFactory = context.getSocketFactory();
 541:       }
 542:     return sslSocketFactory;
 543:   }
 544: 
 545:   void setSSLSocketFactory(SSLSocketFactory factory)
 546:   {
 547:     sslSocketFactory = factory;
 548:   }
 549: 
 550:   protected synchronized InputStream getInputStream()
 551:     throws IOException
 552:   {
 553:     if (socket == null)
 554:       {
 555:         getSocket();
 556:       }
 557:     return in;
 558:   }
 559: 
 560:   protected synchronized OutputStream getOutputStream()
 561:     throws IOException
 562:   {
 563:     if (socket == null)
 564:       {
 565:         getSocket();
 566:       }
 567:     return out;
 568:   }
 569: 
 570:   /**
 571:    * Closes the underlying socket, if any.
 572:    */
 573:   protected synchronized void closeConnection()
 574:     throws IOException
 575:   {
 576:     if (socket != null)
 577:       {
 578:         try
 579:           {
 580:             socket.close();
 581:           }
 582:         finally
 583:           {
 584:             socket = null;
 585:           }
 586:       }
 587:   }
 588: 
 589:   /**
 590:    * Returns a URI representing the connection.
 591:    * This does not include any request path component.
 592:    */
 593:   protected String getURI()
 594:   {
 595:     StringBuilder buf = new StringBuilder();
 596:     buf.append(secure ? "https://" : "http://");
 597:     buf.append(hostname);
 598:     if (secure)
 599:       {
 600:         if (port != HTTPConnection.HTTPS_PORT)
 601:           {
 602:             buf.append(':');
 603:             buf.append(port);
 604:           }
 605:       }
 606:     else
 607:       {
 608:         if (port != HTTPConnection.HTTP_PORT)
 609:           {
 610:             buf.append(':');
 611:             buf.append(port);
 612:           }
 613:       }
 614:     return buf.toString();
 615:   }
 616: 
 617:   /**
 618:    * Get the number of times the specified nonce has been seen by this
 619:    * connection.
 620:    */
 621:   int getNonceCount(String nonce)
 622:   {
 623:     if (nonceCounts == null)
 624:       {
 625:         return 0;
 626:       }
 627:     return((Integer) nonceCounts.get(nonce)).intValue();
 628:   }
 629: 
 630:   /**
 631:    * Increment the number of times the specified nonce has been seen.
 632:    */
 633:   void incrementNonce(String nonce)
 634:   {
 635:     int current = getNonceCount(nonce);
 636:     if (nonceCounts == null)
 637:       {
 638:         nonceCounts = new HashMap();
 639:       }
 640:     nonceCounts.put(nonce, new Integer(current + 1));
 641:   }
 642: 
 643:   // -- Events --
 644:   
 645:   void addHandshakeCompletedListener(HandshakeCompletedListener l)
 646:   {
 647:     synchronized (handshakeCompletedListeners)
 648:       {
 649:         handshakeCompletedListeners.add(l);
 650:       }
 651:   }
 652:   void removeHandshakeCompletedListener(HandshakeCompletedListener l)
 653:   {
 654:     synchronized (handshakeCompletedListeners)
 655:       {
 656:         handshakeCompletedListeners.remove(l);
 657:       }
 658:   }
 659: 
 660: }