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