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

   1: /* HTTPURLConnection.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 java.io.ByteArrayOutputStream;
  42: import java.io.FileNotFoundException;
  43: import java.io.IOException;
  44: import java.io.InputStream;
  45: import java.io.OutputStream;
  46: import java.net.ProtocolException;
  47: import java.net.URL;
  48: import java.security.AccessController;
  49: import java.security.PrivilegedAction;
  50: import java.security.cert.Certificate;
  51: import java.util.Collections;
  52: import java.util.Date;
  53: import java.util.Iterator;
  54: import java.util.LinkedHashMap;
  55: import java.util.Map;
  56: 
  57: import javax.net.ssl.HandshakeCompletedEvent;
  58: import javax.net.ssl.HandshakeCompletedListener;
  59: import javax.net.ssl.HostnameVerifier;
  60: import javax.net.ssl.HttpsURLConnection;
  61: import javax.net.ssl.SSLPeerUnverifiedException;
  62: import javax.net.ssl.SSLSocketFactory;
  63: 
  64: /**
  65:  * A URLConnection that uses the HTTPConnection class.
  66:  *
  67:  * @author Chris Burdess (dog@gnu.org)
  68:  */
  69: public class HTTPURLConnection
  70:   extends HttpsURLConnection
  71:   implements HandshakeCompletedListener
  72: {
  73: 
  74:   /**
  75:    * Pool of reusable connections, used if keepAlive is true.
  76:    */
  77:   private static final LinkedHashMap connectionPool = new LinkedHashMap();
  78:   static int maxConnections;
  79: 
  80:   /*
  81:    * The underlying connection.
  82:    */
  83:   private HTTPConnection connection;
  84: 
  85:   // These are package private for use in anonymous inner classes.
  86:   String proxyHostname;
  87:   int proxyPort;
  88:   String agent;
  89:   boolean keepAlive;
  90: 
  91:   private Request request;
  92:   private Headers requestHeaders;
  93:   private ByteArrayOutputStream requestSink;
  94:   private boolean requestMethodSetExplicitly;
  95: 
  96:   private Response response;
  97:   private InputStream responseSink;
  98:   private InputStream errorSink;
  99: 
 100:   private HandshakeCompletedEvent handshakeEvent;
 101: 
 102:   /**
 103:    * Constructor.
 104:    * @param url the URL
 105:    */
 106:   public HTTPURLConnection(URL url)
 107:     throws IOException
 108:   {
 109:     super(url);
 110:     requestHeaders = new Headers();
 111:     AccessController.doPrivileged(this.new GetHTTPPropertiesAction());
 112:   }
 113: 
 114:   class GetHTTPPropertiesAction
 115:     implements PrivilegedAction
 116:   {
 117: 
 118:     public Object run()
 119:     {
 120:       proxyHostname = System.getProperty("http.proxyHost");
 121:       if (proxyHostname != null && proxyHostname.length() > 0)
 122:         {
 123:           String port = System.getProperty("http.proxyPort");
 124:           if (port != null && port.length() > 0)
 125:             {
 126:               proxyPort = Integer.parseInt(port);
 127:             }
 128:           else
 129:             {
 130:               proxyHostname = null;
 131:               proxyPort = -1;
 132:             }
 133:         }
 134:       agent = System.getProperty("http.agent");
 135:       String ka = System.getProperty("http.keepAlive");
 136:       keepAlive = !(ka != null && "false".equals(ka));
 137:       String mc = System.getProperty("http.maxConnections");
 138:       maxConnections = (mc != null && mc.length() > 0) ?
 139:         Math.max(Integer.parseInt(mc), 1) : 5;
 140:       return null;
 141:     }
 142: 
 143:   }
 144: 
 145:   public void connect()
 146:     throws IOException
 147:   {
 148:     if (connected)
 149:       {
 150:         return;
 151:       }
 152:     String protocol = url.getProtocol();
 153:     boolean secure = "https".equals(protocol);
 154:     String host = url.getHost();
 155:     int port = url.getPort();
 156:     if (port < 0)
 157:       {
 158:         port = secure ? HTTPConnection.HTTPS_PORT :
 159:           HTTPConnection.HTTP_PORT;
 160:       }
 161:     String file = url.getFile();
 162:     String username = url.getUserInfo();
 163:     String password = null;
 164:     if (username != null)
 165:       {
 166:         int ci = username.indexOf(':');
 167:         if (ci != -1)
 168:           {
 169:             password = username.substring(ci + 1);
 170:             username = username.substring(0, ci);
 171:           }
 172:       }
 173:     final Credentials creds = (username == null) ? null :
 174:       new Credentials (username, password);
 175:     
 176:     boolean retry;
 177:     do
 178:       {
 179:         retry = false;
 180:         if (connection == null)
 181:           {
 182:             connection = getConnection(host, port, secure);
 183:             if (secure)
 184:               {
 185:                 SSLSocketFactory factory = getSSLSocketFactory();
 186:                 HostnameVerifier verifier = getHostnameVerifier();
 187:                 if (factory != null)
 188:                   {
 189:                     connection.setSSLSocketFactory(factory);
 190:                   }
 191:                 connection.addHandshakeCompletedListener(this);
 192:                 // TODO verifier
 193:               }
 194:           }
 195:         if (proxyHostname != null)
 196:           {
 197:             if (proxyPort < 0)
 198:               {
 199:                 proxyPort = secure ? HTTPConnection.HTTPS_PORT :
 200:                   HTTPConnection.HTTP_PORT;
 201:               }
 202:             connection.setProxy(proxyHostname, proxyPort);
 203:           }
 204:         try
 205:           {
 206:             request = connection.newRequest(method, file);
 207:             if (!keepAlive)
 208:               {
 209:                 request.setHeader("Connection", "close");
 210:               }
 211:             if (agent != null)
 212:               {
 213:                 request.setHeader("User-Agent", agent);
 214:               }
 215:             request.getHeaders().putAll(requestHeaders);
 216:             if (requestSink != null)
 217:               {
 218:                 byte[] content = requestSink.toByteArray();
 219:                 RequestBodyWriter writer = new ByteArrayRequestBodyWriter(content);
 220:                 request.setRequestBodyWriter(writer);
 221:               }
 222:             if (creds != null)
 223:               {
 224:                 request.setAuthenticator(new Authenticator() {
 225:                     public Credentials getCredentials(String realm, int attempts)
 226:                     {
 227:                       return (attempts < 2) ? creds : null;
 228:                     }
 229:                   });
 230:               }
 231:             response = request.dispatch();
 232:           }
 233:         catch (IOException ioe)
 234:           {
 235:             if (connection.useCount > 0)
 236:               {
 237:                 // Connection re-use failed: Try a new connection.
 238:                 try
 239:                   {
 240:                     connection.close();
 241:                   }
 242:                 catch (IOException _)
 243:                   {
 244:                     // Ignore.
 245:                   }
 246:                 connection = null;
 247:                 retry = true;
 248:                 continue;
 249:               }
 250:             else
 251:               {
 252:                 // First time the connection was used: Hard failure.
 253:                 throw ioe;
 254:               }
 255:           }
 256:         
 257:         if (isRedirect(response) && getInstanceFollowRedirects())
 258:           {
 259:         // Read the response body, if there is one.  If the
 260:         // redirect points us back at the same server, we will use
 261:         // the cached connection, so we must make sure there is no
 262:         // pending data in it.
 263:             InputStream body = response.getBody();
 264:         if (body != null)
 265:           {
 266:         byte[] ignore = new byte[1024];
 267:         while (true)
 268:           {
 269:             int n = body.read(ignore, 0, ignore.length);
 270:             if (n == -1)
 271:               break;
 272:           }
 273:           }
 274: 
 275:             // Follow redirect
 276:             String location = response.getHeader("Location");
 277:         if (location != null)
 278:           {
 279:         String connectionUri = connection.getURI();
 280:         int start = connectionUri.length();
 281:         if (location.startsWith(connectionUri) &&
 282:             location.charAt(start) == '/')
 283:           {
 284:             file = location.substring(start);
 285:             retry = true;
 286:           }
 287:         else if (location.startsWith("http:"))
 288:           {
 289:             connection.close();
 290:             connection = null;
 291:             secure = false;
 292:             start = 7;
 293:             int end = location.indexOf('/', start);
 294:                     if (end == -1)
 295:                       end = location.length();
 296:             host = location.substring(start, end);
 297:             int ci = host.lastIndexOf(':');
 298:             if (ci != -1)
 299:               {
 300:             port = Integer.parseInt(host.substring (ci + 1));
 301:             host = host.substring(0, ci);
 302:               }
 303:             else
 304:               {
 305:             port = HTTPConnection.HTTP_PORT;
 306:               }
 307:             file = location.substring(end);
 308:             retry = true;
 309:           }
 310:         else if (location.startsWith("https:"))
 311:           {
 312:             connection.close();
 313:             connection = null;
 314:             secure = true;
 315:             start = 8;
 316:             int end = location.indexOf('/', start);
 317:                     if (end == -1)
 318:                       end = location.length();
 319:             host = location.substring(start, end);
 320:             int ci = host.lastIndexOf(':');
 321:             if (ci != -1)
 322:               {
 323:             port = Integer.parseInt(host.substring (ci + 1));
 324:             host = host.substring(0, ci);
 325:               }
 326:             else
 327:               {
 328:             port = HTTPConnection.HTTPS_PORT;
 329:               }
 330:             file = location.substring(end);
 331:             retry = true;
 332:           }
 333:         else if (location.length() > 0)
 334:           {
 335:             // Malformed absolute URI, treat as file part of URI
 336:             if (location.charAt(0) == '/')
 337:               {
 338:             // Absolute path
 339:             file = location;
 340:               }
 341:             else
 342:               {
 343:             // Relative path
 344:             int lsi = file.lastIndexOf('/');
 345:             file = (lsi == -1) ? "/" : file.substring(0, lsi + 1);
 346:             file += location;
 347:               }
 348:             retry = true;
 349:           }
 350:           }
 351:           }
 352:         else
 353:           {
 354:             responseSink = response.getBody();
 355:             
 356:             if (response.getCode() == 404)
 357:           {
 358:         errorSink = responseSink;
 359:         throw new FileNotFoundException(url.toString());
 360:           }
 361:           }
 362:       }
 363:     while (retry);
 364:     connected = true;
 365:   }
 366: 
 367:   private static boolean isRedirect(Response response)
 368:   {
 369:     int sc = response.getCode();
 370:     return (sc != 304 && (sc / 100) == 3);
 371:   }
 372: 
 373:   /**
 374:    * Returns a connection, from the pool if necessary.
 375:    */
 376:   HTTPConnection getConnection(String host, int port, boolean secure)
 377:     throws IOException
 378:   {
 379:     HTTPConnection connection;
 380:     if (keepAlive)
 381:       {
 382:         Object key = HTTPConnection.getPoolKey(host, port, secure);
 383:         synchronized (connectionPool)
 384:           {
 385:             connection = (HTTPConnection) connectionPool.remove(key);
 386:             if (connection == null)
 387:               {
 388:                 connection = new HTTPConnection(host, port, secure);
 389:                 connection.setPool(connectionPool);
 390:               }
 391:           }
 392:       }
 393:     else
 394:       {
 395:         connection = new HTTPConnection(host, port, secure);
 396:       }
 397:     return connection;
 398:   }
 399: 
 400:   public void disconnect()
 401:   {
 402:     if (connection != null)
 403:       {
 404:         try
 405:           {
 406:             connection.close();
 407:           }
 408:         catch (IOException e)
 409:           {
 410:           }
 411:       }
 412:   }
 413: 
 414:   public boolean usingProxy()
 415:   {
 416:     return (proxyHostname != null);
 417:   }
 418: 
 419:   /**
 420:    * Overrides the corresponding method in HttpURLConnection to permit
 421:    * arbitrary methods, as long as they're valid ASCII alphabetic
 422:    * characters. This is to permit WebDAV and other HTTP extensions to
 423:    * function.
 424:    * @param method the method
 425:    */
 426:   public void setRequestMethod(String method)
 427:     throws ProtocolException
 428:   {
 429:     if (connected)
 430:       {
 431:         throw new ProtocolException("Already connected");
 432:       }
 433:     // Validate
 434:     method = method.toUpperCase();
 435:     int len = method.length();
 436:     if (len == 0)
 437:       {
 438:         throw new ProtocolException("Empty method name");
 439:       }
 440:     for (int i = 0; i < len; i++)
 441:       {
 442:         char c = method.charAt(i);
 443:         if (c < 0x41 || c > 0x5a)
 444:           {
 445:             throw new ProtocolException("Illegal character '" + c +
 446:                                         "' at index " + i);
 447:           }
 448:       }
 449:     // OK
 450:     this.method = method;
 451:     requestMethodSetExplicitly = true;
 452:   }
 453: 
 454:   public String getRequestProperty(String key)
 455:   {
 456:     return requestHeaders.getValue(key);
 457:   }
 458: 
 459:   public Map getRequestProperties()
 460:   {
 461:     return requestHeaders;
 462:   }
 463: 
 464:   public void setRequestProperty(String key, String value)
 465:   {
 466:     requestHeaders.put(key, value);
 467:   }
 468: 
 469:   public void addRequestProperty(String key, String value)
 470:   {
 471:     String old = requestHeaders.getValue(key);
 472:     if (old == null)
 473:       {
 474:         requestHeaders.put(key, value);
 475:       }
 476:     else
 477:       {
 478:         requestHeaders.put(key, old + "," + value);
 479:       }
 480:   }
 481: 
 482:   public OutputStream getOutputStream()
 483:     throws IOException
 484:   {
 485:     if (connected)
 486:       {
 487:         throw new ProtocolException("Already connected");
 488:       }
 489:     if (!doOutput)
 490:       {
 491:         throw new ProtocolException("doOutput is false");
 492:       }
 493:     else if (!requestMethodSetExplicitly)
 494:       {
 495:         /*
 496:          * Silently change the method to POST if no method was set
 497:          * explicitly. This is due to broken applications depending on this
 498:          * behaviour (Apache XMLRPC for one).
 499:          */
 500:         method = "POST";
 501:       }
 502:     if (requestSink == null)
 503:       {
 504:         requestSink = new ByteArrayOutputStream();
 505:       }
 506:     return requestSink;
 507:   }
 508:   
 509:   // -- Response --
 510:   
 511:   public InputStream getInputStream()
 512:     throws IOException
 513:   {
 514:     if (!connected)
 515:       {
 516:         connect();
 517:       }
 518:     if (!doInput)
 519:       {
 520:         throw new ProtocolException("doInput is false");
 521:       }
 522:     return responseSink;
 523:   }
 524: 
 525:   public InputStream getErrorStream()
 526:   {
 527:     return errorSink;
 528:   }
 529: 
 530:   public Map getHeaderFields()
 531:   {
 532:     if (!connected)
 533:       {
 534:         try
 535:           {
 536:             connect();
 537:           }
 538:         catch (IOException e)
 539:           {
 540:             return null;
 541:           }
 542:       }
 543:     Headers headers = response.getHeaders();
 544:     LinkedHashMap ret = new LinkedHashMap();
 545:     ret.put(null, Collections.singletonList(getStatusLine(response)));
 546:     for (Iterator i = headers.entrySet().iterator(); i.hasNext(); )
 547:       {
 548:         Map.Entry entry = (Map.Entry) i.next();
 549:         String key = (String) entry.getKey();
 550:         String value = (String) entry.getValue();
 551:         ret.put(key, Collections.singletonList(value));
 552:       }
 553:     return Collections.unmodifiableMap(ret);
 554:   }
 555: 
 556:   String getStatusLine(Response response)
 557:   {
 558:     return "HTTP/" + response.getMajorVersion() +
 559:       "." + response.getMinorVersion() +
 560:       " " + response.getCode() +
 561:       " " + response.getMessage();
 562:   }
 563:   
 564:   public String getHeaderField(int index)
 565:   {
 566:     if (!connected)
 567:       {
 568:         try
 569:           {
 570:             connect();
 571:           }
 572:         catch (IOException e)
 573:           {
 574:             return null;
 575:           }
 576:       }
 577:     if (index == 0)
 578:       {
 579:         return getStatusLine(response);
 580:       }
 581:     Iterator i = response.getHeaders().entrySet().iterator();
 582:     Map.Entry entry;
 583:     int count = 1;
 584:     do
 585:       {
 586:         if (!i.hasNext())
 587:           {
 588:             return null;
 589:           }
 590:         entry = (Map.Entry) i.next();
 591:         count++;
 592:       }
 593:     while (count <= index);
 594:     return (String) entry.getValue();
 595:   }
 596: 
 597:   public String getHeaderFieldKey(int index)
 598:   {
 599:     if (!connected)
 600:       {
 601:         try
 602:           {
 603:             connect();
 604:           }
 605:         catch (IOException e)
 606:           {
 607:             return null;
 608:           }
 609:       }
 610:     if (index == 0)
 611:       {
 612:         return null;
 613:       }
 614:     Iterator i = response.getHeaders().entrySet().iterator();
 615:     Map.Entry entry;
 616:     int count = 1;
 617:     do
 618:       {
 619:         if (!i.hasNext())
 620:           {
 621:             return null;
 622:           }
 623:         entry = (Map.Entry) i.next();
 624:         count++;
 625:       }
 626:     while (count <= index);
 627:     return (String) entry.getKey();
 628:   }
 629: 
 630:   public String getHeaderField(String name)
 631:   {
 632:     if (!connected)
 633:       {
 634:         try
 635:           {
 636:             connect();
 637:           }
 638:         catch (IOException e)
 639:           {
 640:             return null;
 641:           }
 642:       }
 643:     return (String) response.getHeader(name);
 644:   }
 645: 
 646:   public long getHeaderFieldDate(String name, long def)
 647:   {
 648:     if (!connected)
 649:       {
 650:         try
 651:           {
 652:             connect();
 653:           }
 654:         catch (IOException e)
 655:           {
 656:             return def;
 657:           }
 658:       }
 659:     Date date = response.getDateHeader(name);
 660:     return (date == null) ? def : date.getTime();
 661:   }
 662: 
 663:   public String getContentType()
 664:   {
 665:     return getHeaderField("Content-Type");
 666:   }
 667: 
 668:   public int getResponseCode()
 669:     throws IOException
 670:   {
 671:     if (!connected)
 672:       {
 673:         connect();
 674:       }
 675:     return response.getCode();
 676:   }
 677: 
 678:   public String getResponseMessage()
 679:     throws IOException
 680:   {
 681:     if (!connected)
 682:       {
 683:         connect();
 684:       }
 685:     return response.getMessage();
 686:   }
 687: 
 688:   // -- HTTPS specific --
 689: 
 690:   public String getCipherSuite()
 691:   {
 692:     if (!connected)
 693:       {
 694:         throw new IllegalStateException("not connected");
 695:       }
 696:     return handshakeEvent.getCipherSuite();
 697:   }
 698:   
 699:   public Certificate[] getLocalCertificates()
 700:   {
 701:     if (!connected)
 702:       {
 703:         throw new IllegalStateException("not connected");
 704:       }
 705:     return handshakeEvent.getLocalCertificates();
 706:   }
 707: 
 708:   public Certificate[] getServerCertificates()
 709:     throws SSLPeerUnverifiedException
 710:   {
 711:     if (!connected)
 712:       {
 713:         throw new IllegalStateException("not connected");
 714:       }
 715:     return handshakeEvent.getPeerCertificates();
 716:   }
 717: 
 718:   // HandshakeCompletedListener
 719: 
 720:   public void handshakeCompleted(HandshakeCompletedEvent event)
 721:   {
 722:     handshakeEvent = event;
 723:   }
 724: 
 725: }