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