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